[
  {
    "path": ".github/workflows/android.yml",
    "content": "name: Loread CI\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v2\n    - name: set up JDK 1.8\n      uses: actions/setup-java@v1\n      with:\n        java-version: 1.8\n    - name: Make Gradle executable\n      run: chmod +x ./gradlew\n    - name: Build with Gradle\n      run: ./gradlew build\n    \n    - name: Release apk\n      uses: ncipollo/release-action@v1.5.0\n      with:\n        artifacts: \"build/app/outputs/apk/release/*.apk\"\n        token: ${{ secrets.GITHUB_RElEASE_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n.DS_Store\n/build\n/.idea\n"
  },
  {
    "path": "README.md",
    "content": "# 序\n\n> 路很长，纵然远望，却不知方向。\n\n> 抽支烟，思绪无常，奔跑着彷徨。\n\n> 逃不脱的苟且，到不了的远方…\n\n\n# 简介\n\nRSS 第三方客户端，支持 Inoreader、Feedly、TinyTinyRSS。\n\n下载地址：[http://www.coolapk.com/apk/168423](http://www.coolapk.com/apk/168423)\n\n\n# 截图\n\n![截图](doc/overview.png)\n\n如上图，从左至右依次为“登录、首页、文章页、分类、快速设置、设置”\n\n\n# 功能\n目前实现以下几个功能：\n\n- [x] 黑夜主题\n- [x] 获取全文：支持根据规则或智能识别全文\n- [x] 保存近期文章的阅读进度\n- [x] 左右切换文章\n- [x] 自动清理过期文章\n- [x] 不同状态下（未读/加星/全部），各分组内文章的数量\n- [x] ~~保存 离线状态下的一些网络请求（文章状态处理，图片下载），待有网再同步~~\n\n对文章列表项的手势操作：\n\n- [x] 左滑是切换文章的“已读/未读”状态\n- [x] 右滑是切换文章的“加星/取消加星”状态\n- [x] 长按是“上面的文章标记为已读，下面的文章标记为已读”\n\nPS：\n\n* 由于开发中本人也还在不断学习，难免有些历史遗留的错误代码以及注释，暂时未被清理，但不影响使用\n\n\n# 后期规划\n### Bug\n- [x] 优化反色算法，解决灰反色问题\n- [ ] 优化音频莫名暂停问题\n- [ ] 优化 ROOM 库带来的问题\n\n### 功能\n- [x] 支持全文搜索\n- [ ] 优化朗读、播放音乐的界面\n- [ ] 支持本地 RSS\n- [ ] 支持获取不支持 RSS 站点的文章\n- [ ] 支持更换主题\n- [ ] 支持设置排版：字体、字号、字距、行距、背景色\n- [ ] 支持长按视频，图片，iframe候展示菜单\n- [ ] 本地训练机器学习模型，判断文章喜好\n- [ ] 检查添加的订阅地址是否有相似的订阅\n\n### 技术\n- [ ] 优化代码结构，拆成不同模块\n- [ ] 改用最新的技术，例如 Kotlin\n- [ ] 使用 CI 自动构建 APK 包\n\n\n# 库的使用\n\n* OkHttp, Gson, ROOM, Glide 等等\n"
  },
  {
    "path": "agentweb-core/.gitignore",
    "content": "/build\n/src/androidTest/\n/src/test/\n"
  },
  {
    "path": "agentweb-core/build.gradle",
    "content": "apply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion 29\n    buildToolsVersion '29.0.3'\n\n    defaultConfig {\n        minSdkVersion 22\n        targetSdkVersion 29\n        versionCode 1\n        versionName \"1.0\"\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n    lintOptions{\n        abortOnError false\n    }\n    repositories {\n        flatDir {\n            dirs 'libs', 'providedLibs'\n        }\n    }\n//   defaultPublishConfig \"debug\"\n\n}\ndependencies {\n    implementation fileTree(include: ['*.jar'], dir: 'libs')\n    androidTestImplementation('androidx.test.espresso:espresso-core:3.2.0', {\n        exclude group: 'com.android.support', module: 'support-annotations'\n    })\n    testImplementation 'junit:junit:4.13'\n    implementation 'com.download.library:Downloader:4.1.2'\n    implementation 'com.google.android.material:material:1.1.0'\n    implementation 'androidx.legacy:legacy-support-v4:1.0.0'\n    implementation fileTree(include: ['*.jar'], dir: 'providedLibs')\n}\n\n"
  },
  {
    "path": "agentweb-core/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          package=\"com.just.agentweb\"\n    >\n\n    <application>\n        <provider\n            android:name=\"com.just.agentweb.AgentWebFileProvider\"\n            android:authorities=\"${applicationId}.AgentWebFileProvider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/web_files_public\"/>\n        </provider>\n        <activity\n            android:name=\"com.just.agentweb.ActionActivity\"\n            android:configChanges=\"keyboardHidden|orientation|screenSize\"\n            android:exported=\"false\"\n            android:launchMode=\"standard\"\n            android:theme=\"@style/actionActivity\"\n            android:windowSoftInputMode=\"stateHidden|stateAlwaysHidden\">\n\n        </activity>\n\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/AbsAgentWebSettings.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\n\nimport android.content.Context;\nimport android.os.Build;\nimport android.view.View;\nimport android.webkit.DownloadListener;\nimport android.webkit.WebChromeClient;\nimport android.webkit.WebSettings;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\n\n/**\n * @author cenxiaozhong\n * @update 4.0.0 ,WebDefaultSettingsManager rename to AbsAgentWebSettings\n * @since 1.0.0\n */\n\npublic abstract class AbsAgentWebSettings implements IAgentWebSettings, WebListenerManager {\n\tprivate WebSettings mWebSettings;\n\tprivate static final String TAG = AbsAgentWebSettings.class.getSimpleName();\n\tpublic static final String USERAGENT_UC = \" UCBrowser/11.6.4.950 \";\n\tpublic static final String USERAGENT_QQ_BROWSER = \" MQQBrowser/8.0 \";\n\tpublic static final String USERAGENT_AGENTWEB = AgentWebConfig.AGENTWEB_VERSION;\n\tprotected AgentWeb mAgentWeb;\n\n\tpublic static AbsAgentWebSettings getInstance() {\n\t\treturn new AgentWebSettingsImpl();\n\t}\n\n\tpublic AbsAgentWebSettings() {\n\t}\n\n\tfinal void bindAgentWeb(AgentWeb agentWeb) {\n\t\tthis.mAgentWeb = agentWeb;\n\t\tthis.bindAgentWebSupport(agentWeb);\n\t}\n\n\tprotected abstract void bindAgentWebSupport(AgentWeb agentWeb);\n\n\t@Override\n\tpublic IAgentWebSettings toSetting(WebView webView) {\n\t\tsettings(webView);\n\t\treturn this;\n\t}\n\n\tprivate void settings(WebView webView) {\n\t\tmWebSettings = webView.getSettings();\n\t\tmWebSettings.setJavaScriptEnabled(true);\n\t\tmWebSettings.setSupportZoom(true);\n\t\tmWebSettings.setBuiltInZoomControls(false);\n\t\tmWebSettings.setSavePassword(false);\n\t\tif (AgentWebUtils.checkNetwork(webView.getContext())) {\n\t\t\t//根据cache-control获取数据。\n\t\t\tmWebSettings.setCacheMode(WebSettings.LOAD_DEFAULT);\n\t\t} else {\n\t\t\t//没网，则从本地获取，即离线加载\n\t\t\tmWebSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);\n\t\t}\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n\t\t\t//适配5.0不允许http和https混合使用情况\n\t\t\tmWebSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);\n\t\t\twebView.setLayerType(View.LAYER_TYPE_HARDWARE, null);\n\t\t} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n\t\t\twebView.setLayerType(View.LAYER_TYPE_HARDWARE, null);\n\t\t} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {\n\t\t\twebView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);\n\t\t}\n\t\tmWebSettings.setTextZoom(100);\n\t\tmWebSettings.setDatabaseEnabled(true);\n\t\tmWebSettings.setAppCacheEnabled(true);\n\t\tmWebSettings.setLoadsImagesAutomatically(true);\n\t\tmWebSettings.setSupportMultipleWindows(false);\n\t\t// 是否阻塞加载网络图片  协议http or https\n\t\tmWebSettings.setBlockNetworkImage(false);\n\t\t// 允许加载本地文件html  file协议\n\t\tmWebSettings.setAllowFileAccess(true);\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {\n\t\t\t// 通过 file url 加载的 Javascript 读取其他的本地文件 .建议关闭\n\t\t\tmWebSettings.setAllowFileAccessFromFileURLs(false);\n\t\t\t// 允许通过 file url 加载的 Javascript 可以访问其他的源，包括其他的文件和 http，https 等其他的源\n\t\t\tmWebSettings.setAllowUniversalAccessFromFileURLs(false);\n\t\t}\n\t\tmWebSettings.setJavaScriptCanOpenWindowsAutomatically(true);\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n\n\t\t\tmWebSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);\n\t\t} else {\n\t\t\tmWebSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);\n\t\t}\n\t\tmWebSettings.setLoadWithOverviewMode(false);\n\t\tmWebSettings.setUseWideViewPort(false);\n\t\tmWebSettings.setDomStorageEnabled(true);\n\t\tmWebSettings.setNeedInitialFocus(true);\n\t\tmWebSettings.setDefaultTextEncodingName(\"utf-8\");//设置编码格式\n\t\tmWebSettings.setDefaultFontSize(16);\n\t\tmWebSettings.setMinimumFontSize(12);//设置 WebView 支持的最小字体大小，默认为 8\n\t\tmWebSettings.setGeolocationEnabled(true);\n\t\tString dir = AgentWebConfig.getCachePath(webView.getContext());\n\t\tLogUtils.i(TAG, \"dir:\" + dir + \"   appcache:\" + AgentWebConfig.getCachePath(webView.getContext()));\n\t\t//设置数据库路径  api19 已经废弃,这里只针对 webkit 起作用\n\t\tmWebSettings.setGeolocationDatabasePath(dir);\n\t\tmWebSettings.setDatabasePath(dir);\n\t\tmWebSettings.setAppCachePath(dir);\n\t\t//缓存文件最大值\n\t\tmWebSettings.setAppCacheMaxSize(Long.MAX_VALUE);\n\t\tmWebSettings.setUserAgentString(getWebSettings()\n\t\t\t\t.getUserAgentString()\n\t\t\t\t.concat(USERAGENT_AGENTWEB)\n\t\t\t\t.concat(USERAGENT_UC)\n\t\t);\n\t\tLogUtils.i(TAG, \"UserAgentString : \" + mWebSettings.getUserAgentString());\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n\t\t\t// 安卓9.0后不允许多进程使用同一个数据目录，需设置前缀来区分\n\t\t\t// 参阅 https://blog.csdn.net/lvshuchangyin/article/details/89446629\n\t\t\tContext context = webView.getContext();\n\t\t\tString processName = ProcessUtils.getCurrentProcessName(context);\n\t\t\tif (!context.getApplicationContext().getPackageName().equals(processName)) {\n\t\t\t\tWebView.setDataDirectorySuffix(processName);\n\t\t\t}\n\t\t}\n\t}\n\n\t@Override\n\tpublic WebSettings getWebSettings() {\n\t\treturn mWebSettings;\n\t}\n\n\t@Override\n\tpublic WebListenerManager setWebChromeClient(WebView webview, WebChromeClient webChromeClient) {\n\t\twebview.setWebChromeClient(webChromeClient);\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic WebListenerManager setWebViewClient(WebView webView, WebViewClient webViewClient) {\n\t\twebView.setWebViewClient(webViewClient);\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic WebListenerManager setDownloader(WebView webView, DownloadListener downloadListener) {\n\t\twebView.setDownloadListener(downloadListener);\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/AbsAgentWebUIController.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.app.Activity;\nimport android.app.Dialog;\nimport android.os.Handler;\nimport android.webkit.JsPromptResult;\nimport android.webkit.JsResult;\nimport android.webkit.WebView;\n\n\n/**\n * 该类统一控制了与用户交互的界面\n *\n * @author cenxiaozhong\n * @since 3.0.0\n */\npublic abstract class AbsAgentWebUIController {\n\n\tpublic static boolean HAS_DESIGN_LIB = false;\n\tprivate Activity mActivity;\n\tprivate WebParentLayout mWebParentLayout;\n\tprivate volatile boolean mIsBindWebParent = false;\n\tprotected AbsAgentWebUIController mAgentWebUIControllerDelegate;\n\tprotected String TAG = this.getClass().getSimpleName();\n\n\tstatic {\n\t\ttry {\n\t\t\tClass.forName(\"android.support.design.widget.Snackbar\");\n\t\t\tClass.forName(\"android.support.design.widget.BottomSheetDialog\");\n\t\t\tHAS_DESIGN_LIB = true;\n\t\t} catch (Throwable ignore) {\n\t\t\tHAS_DESIGN_LIB = false;\n\t\t}\n\t}\n\n\tprotected AbsAgentWebUIController create() {\n\t\treturn HAS_DESIGN_LIB ? new DefaultDesignUIController() : new DefaultUIController();\n\t}\n\n\tprotected AbsAgentWebUIController getDelegate() {\n\t\tAbsAgentWebUIController mAgentWebUIController = this.mAgentWebUIControllerDelegate;\n\t\tif (mAgentWebUIController == null) {\n\t\t\tthis.mAgentWebUIControllerDelegate = mAgentWebUIController = create();\n\t\t}\n\t\treturn mAgentWebUIController;\n\t}\n\n\tfinal synchronized void bindWebParent(WebParentLayout webParentLayout, Activity activity) {\n\t\tif (!mIsBindWebParent) {\n\t\t\tmIsBindWebParent = true;\n\t\t\tthis.mWebParentLayout = webParentLayout;\n\t\t\tthis.mActivity = activity;\n\t\t\tbindSupportWebParent(webParentLayout, activity);\n\t\t}\n\t}\n\n\tprotected void toDismissDialog(Dialog dialog) {\n\t\tif (dialog != null && dialog.isShowing()) {\n\t\t\tdialog.dismiss();\n\t\t}\n\t}\n\n\tprotected void toShowDialog(Dialog dialog) {\n\t\tif (dialog != null && !dialog.isShowing()) {\n\t\t\tdialog.show();\n\t\t}\n\t}\n\n\tprotected abstract void bindSupportWebParent(WebParentLayout webParentLayout, Activity activity);\n\n\t/**\n\t * WebChromeClient#onJsAlert\n\t *\n\t * @param view\n\t * @param url\n\t * @param message\n\t */\n\tpublic abstract void onJsAlert(WebView view, String url, String message);\n\n\t/**\n\t * 咨询用户是否前往其他页面\n\t *\n\t * @param view\n\t * @param url\n\t * @param callback\n\t */\n\tpublic abstract void onOpenPagePrompt(WebView view, String url, Handler.Callback callback);\n\n\t/**\n\t * WebChromeClient#onJsConfirm\n\t *\n\t * @param view\n\t * @param url\n\t * @param message\n\t * @param jsResult\n\t */\n\tpublic abstract void onJsConfirm(WebView view, String url, String message, JsResult jsResult);\n\n\tpublic abstract void onSelectItemsPrompt(WebView view, String url, String[] ways, Handler.Callback callback);\n\n\t/**\n\t * 强制下载弹窗\n\t *\n\t * @param url      当前下载地址。\n\t * @param callback 用户操作回调回调\n\t */\n\tpublic abstract void onForceDownloadAlert(String url, Handler.Callback callback);\n\n\t/**\n\t * WebChromeClient#onJsPrompt\n\t *\n\t * @param view\n\t * @param url\n\t * @param message\n\t * @param defaultValue\n\t * @param jsPromptResult\n\t */\n\tpublic abstract void onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult jsPromptResult);\n\n\t/**\n\t * 显示错误页\n\t *\n\t * @param view\n\t * @param errorCode\n\t * @param description\n\t * @param failingUrl\n\t */\n\tpublic abstract void onMainFrameError(WebView view, int errorCode, String description, String failingUrl);\n\n\t/**\n\t * 隐藏错误页\n\t */\n\tpublic abstract void onShowMainFrame();\n\n\t/**\n\t * 正在加载...\n\t *\n\t * @param msg\n\t */\n\tpublic abstract void onLoading(String msg);\n\n\t/**\n\t * 取消正在加载...\n\t */\n\tpublic abstract void onCancelLoading();\n\n\t/**\n\t * @param message 消息\n\t * @param intent  说明message的来源，意图\n\t */\n\tpublic abstract void onShowMessage(String message, String intent);\n\n\t/**\n\t * 当权限被拒回调该方法\n\t *\n\t * @param permissions\n\t * @param permissionType\n\t * @param action\n\t */\n\tpublic abstract void onPermissionsDeny(String[] permissions, String permissionType, String action);\n\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/Action.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n\n/**\n * @author cenxiaozhong\n * @since 2.0.0\n */\npublic class Action implements Parcelable {\n\n\tpublic transient static final int ACTION_PERMISSION = 1;\n\tpublic transient static final int ACTION_FILE = 2;\n\tpublic transient static final int ACTION_CAMERA = 3;\n\tpublic transient static final int ACTION_VIDEO = 4;\n\tprivate ArrayList<String> mPermissions = new ArrayList();\n\tprivate int mAction;\n\tprivate int mFromIntention;\n\n\tpublic Action() {\n\t}\n\n\tpublic ArrayList<String> getPermissions() {\n\t\treturn mPermissions;\n\t}\n\n\tpublic void setPermissions(ArrayList<String> permissions) {\n\t\tthis.mPermissions = permissions;\n\t}\n\n\tpublic void setPermissions(String[] permissions) {\n\t\tthis.mPermissions = new ArrayList<>(Arrays.asList(permissions));\n\t}\n\n\tpublic int getAction() {\n\t\treturn mAction;\n\t}\n\n\tpublic void setAction(int action) {\n\t\tthis.mAction = action;\n\t}\n\n\tprotected Action(Parcel in) {\n\t\tmPermissions = in.createStringArrayList();\n\t\tmAction = in.readInt();\n\t\tmFromIntention = in.readInt();\n\t}\n\n\t@Override\n\tpublic void writeToParcel(Parcel dest, int flags) {\n\t\tdest.writeStringList(mPermissions);\n\t\tdest.writeInt(mAction);\n\t\tdest.writeInt(mFromIntention);\n\t}\n\n\t@Override\n\tpublic int describeContents() {\n\t\treturn 0;\n\t}\n\n\tpublic static final Creator<Action> CREATOR = new Creator<Action>() {\n\t\t@Override\n\t\tpublic Action createFromParcel(Parcel in) {\n\t\t\treturn new Action(in);\n\t\t}\n\n\t\t@Override\n\t\tpublic Action[] newArray(int size) {\n\t\t\treturn new Action[size];\n\t\t}\n\t};\n\n\tpublic int getFromIntention() {\n\t\treturn mFromIntention;\n\t}\n\n\tpublic static Action createPermissionsAction(String[] permissions) {\n\t\tAction mAction = new Action();\n\t\tmAction.setAction(Action.ACTION_PERMISSION);\n\t\tList<String> mList = Arrays.asList(permissions);\n\t\tmAction.setPermissions(new ArrayList<String>(mList));\n\t\treturn mAction;\n\t}\n\n\tpublic Action setFromIntention(int fromIntention) {\n\t\tthis.mFromIntention = fromIntention;\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/ActionActivity.java",
    "content": "\n/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.app.Activity;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.os.Bundle;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport java.io.File;\nimport java.util.List;\n\nimport static android.provider.MediaStore.EXTRA_OUTPUT;\n\n\n\n/**\n * @since 2.0.0\n * @author cenxiaozhong\n */\npublic final class ActionActivity extends Activity {\n\n    public static final String KEY_ACTION = \"KEY_ACTION\";\n    public static final String KEY_URI = \"KEY_URI\";\n    public static final String KEY_FROM_INTENTION = \"KEY_FROM_INTENTION\";\n    public static final String KEY_FILE_CHOOSER_INTENT = \"KEY_FILE_CHOOSER_INTENT\";\n    private static RationaleListener mRationaleListener;\n    private static PermissionListener mPermissionListener;\n    private static ChooserListener mChooserListener;\n    private static final String TAG = ActionActivity.class.getSimpleName();\n    private Action mAction;\n    public static final int REQUEST_CODE = 0x254;\n\n    public static void start(Activity activity, Action action) {\n        Intent mIntent = new Intent(activity, ActionActivity.class);\n        mIntent.putExtra(KEY_ACTION, action);\n//        mIntent.setExtrasClassLoader(Action.class.getClassLoader());\n        activity.startActivity(mIntent);\n\n    }\n\n    public static void setChooserListener(ChooserListener chooserListener) {\n        mChooserListener = chooserListener;\n    }\n\n    public static void setPermissionListener(PermissionListener permissionListener) {\n        mPermissionListener = permissionListener;\n    }\n\n    private void cancelAction() {\n        mChooserListener = null;\n        mPermissionListener = null;\n        mRationaleListener = null;\n    }\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        if (savedInstanceState != null) {\n            LogUtils.i(TAG, \"savedInstanceState:\" + savedInstanceState);\n            return;\n        }\n        Intent intent = getIntent();\n        mAction = intent.getParcelableExtra(KEY_ACTION);\n        if (mAction == null) {\n            cancelAction();\n            this.finish();\n            return;\n        }\n        if (mAction.getAction() == Action.ACTION_PERMISSION) {\n            permission(mAction);\n        } else if (mAction.getAction() == Action.ACTION_CAMERA) {\n            realOpenCamera();\n        } else if (mAction.getAction() == Action.ACTION_VIDEO){\n            realOpenVideo();\n        } else {\n            fetchFile(mAction);\n        }\n    }\n\n    private void fetchFile(Action action) {\n        if (mChooserListener == null) {\n            finish();\n        }\n        realOpenFileChooser();\n    }\n\n    private void realOpenFileChooser() {\n        try {\n            if (mChooserListener == null) {\n                finish();\n                return;\n            }\n            Intent mIntent = getIntent().getParcelableExtra(KEY_FILE_CHOOSER_INTENT);\n            if (mIntent == null) {\n                cancelAction();\n                return;\n            }\n            this.startActivityForResult(mIntent, REQUEST_CODE);\n        } catch (Throwable throwable) {\n            LogUtils.i(TAG, \"找不到文件选择器\");\n            chooserActionCallback(-1, null);\n            if (LogUtils.isDebug()) {\n                throwable.printStackTrace();\n            }\n        }\n    }\n\n    private void chooserActionCallback(int resultCode, Intent data) {\n        if (mChooserListener != null) {\n            mChooserListener.onChoiceResult(REQUEST_CODE, resultCode, data);\n            mChooserListener = null;\n        }\n        finish();\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\n        if (requestCode == REQUEST_CODE) {\n            chooserActionCallback(resultCode, mUri != null ? new Intent().putExtra(KEY_URI, mUri) : data);\n        }\n    }\n\n    private void permission(Action action) {\n        List<String> permissions = action.getPermissions();\n        if (AgentWebUtils.isEmptyCollection(permissions)) {\n            mPermissionListener = null;\n            mRationaleListener = null;\n            finish();\n            return;\n        }\n        if (mRationaleListener != null) {\n            boolean rationale = false;\n            for (String permission : permissions) {\n                rationale = shouldShowRequestPermissionRationale(permission);\n                if (rationale) {\n                    break;\n                }\n            }\n            mRationaleListener.onRationaleResult(rationale, new Bundle());\n            mRationaleListener = null;\n            finish();\n            return;\n        }\n        if (mPermissionListener != null){\n            requestPermissions(permissions.toArray(new String[]{}), 1);\n        }\n    }\n\n    private Uri mUri;\n\n    private void realOpenCamera() {\n        try {\n            if (mChooserListener == null){\n                finish();\n            }\n            File mFile = AgentWebUtils.createImageFile(this);\n            if (mFile == null) {\n                mChooserListener.onChoiceResult(REQUEST_CODE, Activity.RESULT_CANCELED, null);\n                mChooserListener = null;\n                finish();\n            }\n            Intent intent = AgentWebUtils.getIntentCaptureCompat(this, mFile);\n            // 指定开启系统相机的Action\n            mUri = intent.getParcelableExtra(EXTRA_OUTPUT);\n            this.startActivityForResult(intent, REQUEST_CODE);\n        } catch (Throwable ignore) {\n            LogUtils.e(TAG, \"找不到系统相机\");\n            if (mChooserListener != null) {\n                mChooserListener.onChoiceResult(REQUEST_CODE, Activity.RESULT_CANCELED, null);\n            }\n            mChooserListener = null;\n            if (LogUtils.isDebug()){\n                ignore.printStackTrace();\n            }\n        }\n    }\n\n    private void realOpenVideo(){\n        try {\n            if (mChooserListener == null){\n                finish();\n            }\n            File mFile = AgentWebUtils.createVideoFile(this);\n            if (mFile == null) {\n                mChooserListener.onChoiceResult(REQUEST_CODE, Activity.RESULT_CANCELED, null);\n                mChooserListener = null;\n                finish();\n            }\n            Intent intent = AgentWebUtils.getIntentVideoCompat(this, mFile);\n            // 指定开启系统相机的Action\n            mUri = intent.getParcelableExtra(EXTRA_OUTPUT);\n            this.startActivityForResult(intent, REQUEST_CODE);\n        } catch (Throwable ignore) {\n            LogUtils.e(TAG, \"找不到系统相机\");\n            if (mChooserListener != null) {\n                mChooserListener.onChoiceResult(REQUEST_CODE, Activity.RESULT_CANCELED, null);\n            }\n            mChooserListener = null;\n            if (LogUtils.isDebug()){\n                ignore.printStackTrace();\n            }\n        }\n    }\n\n\n    @Override\n    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {\n        if (mPermissionListener != null) {\n            Bundle mBundle = new Bundle();\n            mBundle.putInt(KEY_FROM_INTENTION, mAction.getFromIntention());\n            mPermissionListener.onRequestPermissionsResult(permissions, grantResults, mBundle);\n        }\n        mPermissionListener = null;\n        finish();\n    }\n\n    public interface RationaleListener {\n        void onRationaleResult(boolean showRationale, Bundle extras);\n    }\n\n    public interface PermissionListener {\n        void onRequestPermissionsResult(@NonNull String[] permissions, @NonNull int[] grantResults, Bundle extras);\n    }\n\n    public interface ChooserListener {\n        void onChoiceResult(int requestCode, int resultCode, Intent data);\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/AgentWeb.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.app.Activity;\nimport android.text.TextUtils;\nimport android.view.KeyEvent;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.webkit.WebChromeClient;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.IdRes;\nimport androidx.annotation.LayoutRes;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.collection.ArrayMap;\nimport androidx.fragment.app.Fragment;\n\nimport java.lang.ref.WeakReference;\nimport java.util.Map;\n\n/**\n * @author cenxiaozhong\n * @update 4.0.0\n * @since 1.0.0\n */\npublic final class AgentWeb {\n\t/**\n\t * AgentWeb 's TAG\n\t */\n\tprivate static final String TAG = AgentWeb.class.getSimpleName();\n\t/**\n\t * Activity\n\t */\n\tprivate Activity mActivity;\n\t/**\n\t * 承载 WebParentLayout 的 ViewGroup\n\t */\n\tprivate ViewGroup mViewGroup;\n\t/**\n\t * 负责创建布局 WebView ，WebParentLayout  Indicator等。\n\t */\n\tprivate WebCreator mWebCreator;\n\t/**\n\t * 管理 WebSettings\n\t */\n\tprivate IAgentWebSettings mAgentWebSettings;\n\t/**\n\t * AgentWeb\n\t */\n\tprivate AgentWeb mAgentWeb = null;\n\t/**\n\t * IndicatorController 控制Indicator\n\t */\n\tprivate IndicatorController mIndicatorController;\n\t/**\n\t * WebChromeClient\n\t */\n\tprivate com.just.agentweb.WebChromeClient mWebChromeClient;\n\t/**\n\t * WebViewClient\n\t */\n\tprivate com.just.agentweb.WebViewClient mWebViewClient;\n\t/**\n\t * is show indicator\n\t */\n\tprivate boolean mEnableIndicator;\n\t/**\n\t * IEventHandler 处理WebView相关返回事件\n\t */\n\tprivate IEventHandler mIEventHandler;\n\t/**\n\t * WebView 注入对象\n\t */\n\tprivate ArrayMap<String, Object> mJavaObjects = new ArrayMap<>();\n\t/**\n\t * flag\n\t */\n\tprivate int TAG_TARGET = 0;\n\t/**\n\t * WebListenerManager\n\t */\n\tprivate WebListenerManager mWebListenerManager;\n\t/**\n\t * 安全 Controller\n\t */\n\tprivate WebSecurityController<WebSecurityCheckLogic> mWebSecurityController = null;\n\t/**\n\t * WebSecurityCheckLogic\n\t */\n\tprivate WebSecurityCheckLogic mWebSecurityCheckLogic = null;\n\t/**\n\t * WebChromeClient\n\t */\n\tprivate WebChromeClient mTargetChromeClient;\n\t/**\n\t * flag security 's mode\n\t */\n\tprivate SecurityType mSecurityType = SecurityType.DEFAULT_CHECK;\n\t/**\n\t * Activity\n\t */\n\tprivate static final int ACTIVITY_TAG = 0;\n\t/**\n\t * Fragment\n\t */\n\tprivate static final int FRAGMENT_TAG = 1;\n\t/**\n\t * AgentWeb 默认注入对像\n\t */\n\tprivate AgentWebJsInterfaceCompat mAgentWebJsInterfaceCompat = null;\n\t/**\n\t * JsAccessEntrace 提供快速JS方法调用\n\t */\n\tprivate JsAccessEntrace mJsAccessEntrace = null;\n\t/**\n\t * URL Loader ， 提供了 WebView#loadUrl(url) reload() stopLoading（） postUrl()等方法\n\t */\n\tprivate IUrlLoader mIUrlLoader = null;\n\t/**\n\t * WebView 生命周期 ， 跟随生命周期释放CPU\n\t */\n\tprivate WebLifeCycle mWebLifeCycle;\n\t/**\n\t * Video 视屏播放管理类\n\t */\n\tprivate IVideo mIVideo = null;\n\t/**\n\t * WebViewClient 辅助控制开关\n\t */\n\tprivate boolean mWebClientHelper = true;\n\t/**\n\t * PermissionInterceptor 权限拦截\n\t */\n\tprivate PermissionInterceptor mPermissionInterceptor;\n\t/**\n\t * 是否拦截未知的Url， @link{DefaultWebClient}\n\t */\n\tprivate boolean mIsInterceptUnkownUrl = false;\n\tprivate int mUrlHandleWays = -1;\n\t/**\n\t * MiddlewareWebClientBase WebViewClient 中间件\n\t */\n\tprivate MiddlewareWebClientBase mMiddleWrareWebClientBaseHeader;\n\t/**\n\t * MiddlewareWebChromeBase WebChromeClient 中间件\n\t */\n\tprivate MiddlewareWebChromeBase mMiddlewareWebChromeBaseHeader;\n\t/**\n\t * 事件拦截\n\t */\n\tprivate EventInterceptor mEventInterceptor;\n\t/**\n\t * 注入对象管理类\n\t */\n\tprivate JsInterfaceHolder mJsInterfaceHolder = null;\n\n\n\tprivate AgentWeb(AgentBuilder agentBuilder) {\n\t\tTAG_TARGET = agentBuilder.mTag;\n\t\tthis.mActivity = agentBuilder.mActivity;\n\t\tthis.mViewGroup = agentBuilder.mViewGroup;\n\t\tthis.mIEventHandler = agentBuilder.mIEventHandler;\n\t\tthis.mEnableIndicator = agentBuilder.mEnableIndicator;\n\t\tmWebCreator = agentBuilder.mWebCreator == null ? configWebCreator(agentBuilder.mBaseIndicatorView, agentBuilder.mIndex, agentBuilder.mLayoutParams, agentBuilder.mIndicatorColor, agentBuilder.mHeight, agentBuilder.mWebView, agentBuilder.mWebLayout) : agentBuilder.mWebCreator;\n\t\tmIndicatorController = agentBuilder.mIndicatorController;\n\t\tthis.mWebChromeClient = agentBuilder.mWebChromeClient;\n\t\tthis.mWebViewClient = agentBuilder.mWebViewClient;\n\t\tmAgentWeb = this;\n\t\tthis.mAgentWebSettings = agentBuilder.mAgentWebSettings;\n\n\t\tif (agentBuilder.mJavaObject != null && !agentBuilder.mJavaObject.isEmpty()) {\n\t\t\tthis.mJavaObjects.putAll((Map<? extends String, ?>) agentBuilder.mJavaObject);\n\t\t\tLogUtils.i(TAG, \"mJavaObject size:\" + this.mJavaObjects.size());\n\n\t\t}\n\t\tthis.mPermissionInterceptor = agentBuilder.mPermissionInterceptor == null ? null : new PermissionInterceptorWrapper(agentBuilder.mPermissionInterceptor);\n\t\tthis.mSecurityType = agentBuilder.mSecurityType;\n\t\tthis.mIUrlLoader = new UrlLoaderImpl(mWebCreator.create().getWebView(), agentBuilder.mHttpHeaders);\n\t\tif (this.mWebCreator.getWebParentLayout() instanceof WebParentLayout) {\n\t\t\tWebParentLayout mWebParentLayout = (WebParentLayout) this.mWebCreator.getWebParentLayout();\n\t\t\tmWebParentLayout.bindController(agentBuilder.mAgentWebUIController == null ? AgentWebUIControllerImplBase.build() : agentBuilder.mAgentWebUIController);\n\t\t\tmWebParentLayout.setErrorLayoutRes(agentBuilder.mErrorLayout, agentBuilder.mReloadId);\n\t\t\tmWebParentLayout.setErrorView(agentBuilder.mErrorView);\n\t\t}\n\t\tthis.mWebLifeCycle = new DefaultWebLifeCycleImpl(mWebCreator.getWebView());\n\t\tmWebSecurityController = new WebSecurityControllerImpl(mWebCreator.getWebView(), this.mAgentWeb.mJavaObjects, this.mSecurityType);\n\t\tthis.mWebClientHelper = agentBuilder.mWebClientHelper;\n\t\tthis.mIsInterceptUnkownUrl = agentBuilder.mIsInterceptUnkownUrl;\n\t\tif (agentBuilder.mOpenOtherPage != null) {\n\t\t\tthis.mUrlHandleWays = agentBuilder.mOpenOtherPage.code;\n\t\t}\n\t\tthis.mMiddleWrareWebClientBaseHeader = agentBuilder.mMiddlewareWebClientBaseHeader;\n\t\tthis.mMiddlewareWebChromeBaseHeader = agentBuilder.mChromeMiddleWareHeader;\n\t\tinit();\n\t}\n\n\n\t/**\n\t * @return PermissionInterceptor 权限控制者\n\t */\n\tpublic PermissionInterceptor getPermissionInterceptor() {\n\t\treturn this.mPermissionInterceptor;\n\t}\n\n\tpublic WebLifeCycle getWebLifeCycle() {\n\t\treturn this.mWebLifeCycle;\n\t}\n\n\tpublic JsAccessEntrace getJsAccessEntrace() {\n\t\tJsAccessEntrace mJsAccessEntrace = this.mJsAccessEntrace;\n\t\tif (mJsAccessEntrace == null) {\n\t\t\tthis.mJsAccessEntrace = mJsAccessEntrace = JsAccessEntraceImpl.getInstance(mWebCreator.getWebView());\n\t\t}\n\t\treturn mJsAccessEntrace;\n\t}\n\n\n\tpublic AgentWeb clearWebCache() {\n\t\tif (this.getWebCreator().getWebView() != null) {\n\t\t\tAgentWebUtils.clearWebViewAllCache(mActivity, this.getWebCreator().getWebView());\n\t\t} else {\n\t\t\tAgentWebUtils.clearWebViewAllCache(mActivity);\n\t\t}\n\t\treturn this;\n\t}\n\n\n\tpublic static AgentBuilder with(@NonNull Activity activity) {\n\t\tif (activity == null) {\n\t\t\tthrow new NullPointerException(\"activity can not be null .\");\n\t\t}\n\t\treturn new AgentBuilder(activity);\n\t}\n\n\tpublic static AgentBuilder with(@NonNull Fragment fragment) {\n\t\tActivity mActivity = null;\n\t\tif ((mActivity = fragment.getActivity()) == null) {\n\t\t\tthrow new NullPointerException(\"activity can not be null .\");\n\t\t}\n\t\treturn new AgentBuilder(mActivity, fragment);\n\t}\n\n\tpublic boolean handleKeyEvent(int keyCode, KeyEvent keyEvent) {\n\t\tif (mIEventHandler == null) {\n\t\t\tmIEventHandler = EventHandlerImpl.getInstantce(mWebCreator.getWebView(), getInterceptor());\n\t\t}\n\t\treturn mIEventHandler.onKeyDown(keyCode, keyEvent);\n\t}\n\n\tpublic boolean back() {\n\t\tif (mIEventHandler == null) {\n\t\t\tmIEventHandler = EventHandlerImpl.getInstantce(mWebCreator.getWebView(), getInterceptor());\n\t\t}\n\t\treturn mIEventHandler.back();\n\t}\n\n\n\tpublic WebCreator getWebCreator() {\n\t\treturn this.mWebCreator;\n\t}\n\n\tpublic IEventHandler getIEventHandler() {\n\t\treturn this.mIEventHandler == null ? (this.mIEventHandler = EventHandlerImpl.getInstantce(mWebCreator.getWebView(), getInterceptor())) : this.mIEventHandler;\n\t}\n\n\n\tpublic IAgentWebSettings getAgentWebSettings() {\n\t\treturn this.mAgentWebSettings;\n\t}\n\n\tpublic IndicatorController getIndicatorController() {\n\t\treturn this.mIndicatorController;\n\t}\n\n\tpublic JsInterfaceHolder getJsInterfaceHolder() {\n\t\treturn this.mJsInterfaceHolder;\n\t}\n\n\tpublic IUrlLoader getUrlLoader() {\n\t\treturn this.mIUrlLoader;\n\t}\n\n\tpublic void destroy() {\n\t\tthis.mWebLifeCycle.onDestroy();\n\t}\n\n\tpublic static class PreAgentWeb {\n\t\tprivate AgentWeb mAgentWeb;\n\t\tprivate boolean isReady = false;\n\n\t\tPreAgentWeb(AgentWeb agentWeb) {\n\t\t\tthis.mAgentWeb = agentWeb;\n\t\t}\n\n\t\tpublic PreAgentWeb ready() {\n\t\t\tif (!isReady) {\n\t\t\t\tmAgentWeb.ready();\n\t\t\t\tisReady = true;\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic AgentWeb get() {\n\t\t\tready();\n\t\t\treturn mAgentWeb;\n\t\t}\n\n\t\tpublic AgentWeb go(@Nullable String url) {\n\t\t\tif (!isReady) {\n\t\t\t\tready();\n\t\t\t}\n\t\t\treturn mAgentWeb.go(url);\n\t\t}\n\t}\n\n\tprivate void doSafeCheck() {\n\t\tWebSecurityCheckLogic mWebSecurityCheckLogic = this.mWebSecurityCheckLogic;\n\t\tif (mWebSecurityCheckLogic == null) {\n\t\t\tthis.mWebSecurityCheckLogic = mWebSecurityCheckLogic = WebSecurityLogicImpl.getInstance();\n\t\t}\n\t\tmWebSecurityController.check(mWebSecurityCheckLogic);\n\t}\n\n\tprivate void doCompat() {\n\t\tmJavaObjects.put(\"agentWeb\", mAgentWebJsInterfaceCompat = new AgentWebJsInterfaceCompat(this, mActivity));\n\t}\n\n\tprivate WebCreator configWebCreator(BaseIndicatorView progressView, int index, ViewGroup.LayoutParams lp, int indicatorColor, int height_dp, WebView webView, IWebLayout webLayout) {\n\t\tif (progressView != null && mEnableIndicator) {\n\t\t\treturn new DefaultWebCreator(mActivity, mViewGroup, lp, index, progressView, webView, webLayout);\n\t\t} else {\n\t\t\treturn mEnableIndicator ?\n\t\t\t\t\tnew DefaultWebCreator(mActivity, mViewGroup, lp, index, indicatorColor, height_dp, webView, webLayout)\n\t\t\t\t\t: new DefaultWebCreator(mActivity, mViewGroup, lp, index, webView, webLayout);\n\t\t}\n\t}\n\n\tprivate AgentWeb go(String url) {\n\t\tthis.getUrlLoader().loadUrl(url);\n\t\tIndicatorController mIndicatorController = null;\n\t\tif (!TextUtils.isEmpty(url) && (mIndicatorController = getIndicatorController()) != null && mIndicatorController.offerIndicator() != null) {\n\t\t\tgetIndicatorController().offerIndicator().show();\n\t\t}\n\t\treturn this;\n\t}\n\n\tprivate EventInterceptor getInterceptor() {\n\t\tif (this.mEventInterceptor != null) {\n\t\t\treturn this.mEventInterceptor;\n\t\t}\n\t\tif (mIVideo instanceof VideoImpl) {\n\t\t\treturn this.mEventInterceptor = (EventInterceptor) this.mIVideo;\n\t\t}\n\t\treturn null;\n\t}\n\n\tprivate void init() {\n\t\tdoCompat();\n\t\tdoSafeCheck();\n\t}\n\n\tprivate IVideo getIVideo() {\n\t\treturn mIVideo == null ? new VideoImpl(mActivity, mWebCreator.getWebView()) : mIVideo;\n\t}\n\n\tprivate WebViewClient getWebViewClient() {\n\n\t\tLogUtils.i(TAG, \"getDelegate:\" + this.mMiddleWrareWebClientBaseHeader);\n\t\tDefaultWebClient mDefaultWebClient = DefaultWebClient\n\t\t\t\t.createBuilder()\n\t\t\t\t.setActivity(this.mActivity)\n\t\t\t\t.setWebClientHelper(this.mWebClientHelper)\n\t\t\t\t.setPermissionInterceptor(this.mPermissionInterceptor)\n\t\t\t\t.setWebView(this.mWebCreator.getWebView())\n\t\t\t\t.setInterceptUnkownUrl(this.mIsInterceptUnkownUrl)\n\t\t\t\t.setUrlHandleWays(this.mUrlHandleWays)\n\t\t\t\t.build();\n\t\tMiddlewareWebClientBase header = this.mMiddleWrareWebClientBaseHeader;\n\t\tif (this.mWebViewClient != null) {\n\t\t\tthis.mWebViewClient.enq(this.mMiddleWrareWebClientBaseHeader);\n\t\t\theader = this.mWebViewClient;\n\t\t}\n\t\tif (header != null) {\n\t\t\tMiddlewareWebClientBase tail = header;\n\t\t\tint count = 1;\n\t\t\tMiddlewareWebClientBase tmp = header;\n\t\t\twhile (tmp.next() != null) {\n\t\t\t\ttail = tmp = tmp.next();\n\t\t\t\tcount++;\n\t\t\t}\n\t\t\tLogUtils.i(TAG, \"MiddlewareWebClientBase middleware count:\" + count);\n\t\t\ttail.setDelegate(mDefaultWebClient);\n\t\t\treturn header;\n\t\t} else {\n\t\t\treturn mDefaultWebClient;\n\t\t}\n\t}\n\n\tprivate AgentWeb ready() {\n\t\tAgentWebConfig.initCookiesManager(mActivity.getApplicationContext());\n\t\tIAgentWebSettings mAgentWebSettings = this.mAgentWebSettings;\n\t\tif (mAgentWebSettings == null) {\n\t\t\tthis.mAgentWebSettings = mAgentWebSettings = AgentWebSettingsImpl.getInstance();\n\t\t}\n\t\tif (mAgentWebSettings instanceof AbsAgentWebSettings) {\n\t\t\t((AbsAgentWebSettings) mAgentWebSettings).bindAgentWeb(this);\n\t\t}\n\t\tif (mWebListenerManager == null && mAgentWebSettings instanceof AbsAgentWebSettings) {\n\t\t\tmWebListenerManager = (WebListenerManager) mAgentWebSettings;\n\t\t}\n\t\tmAgentWebSettings.toSetting(mWebCreator.getWebView());\n\t\tif (mJsInterfaceHolder == null) {\n\t\t\tmJsInterfaceHolder = JsInterfaceHolderImpl.getJsInterfaceHolder(mWebCreator.getWebView(), this.mSecurityType);\n\t\t}\n\t\tLogUtils.i(TAG, \"mJavaObjects:\" + mJavaObjects.size());\n\t\tif (mJavaObjects != null && !mJavaObjects.isEmpty()) {\n\t\t\tmJsInterfaceHolder.addJavaObjects(mJavaObjects);\n\t\t}\n\t\tif (mWebListenerManager != null) {\n\t\t\tmWebListenerManager.setDownloader(mWebCreator.getWebView(), null);\n\t\t\tmWebListenerManager.setWebChromeClient(mWebCreator.getWebView(), getChromeClient());\n\t\t\tmWebListenerManager.setWebViewClient(mWebCreator.getWebView(), getWebViewClient());\n\t\t}\n\t\treturn this;\n\t}\n\n\tprivate WebChromeClient getChromeClient() {\n\t\tIndicatorController mIndicatorController =\n\t\t\t\t(this.mIndicatorController == null) ?\n\t\t\t\t\t\tIndicatorHandler.getInstance().inJectIndicator(mWebCreator.offer())\n\t\t\t\t\t\t: this.mIndicatorController;\n\n\t\tDefaultChromeClient mDefaultChromeClient =\n\t\t\t\tnew DefaultChromeClient(this.mActivity,\n\t\t\t\t\t\tthis.mIndicatorController = mIndicatorController,\n\t\t\t\t\t\tnull, this.mIVideo = getIVideo(),\n\t\t\t\t\t\tthis.mPermissionInterceptor, mWebCreator.getWebView());\n\n\t\tLogUtils.i(TAG, \"WebChromeClient:\" + this.mWebChromeClient);\n\t\tMiddlewareWebChromeBase header = this.mMiddlewareWebChromeBaseHeader;\n\t\tif (this.mWebChromeClient != null) {\n\t\t\tthis.mWebChromeClient.enq(header);\n\t\t\theader = this.mWebChromeClient;\n\t\t}\n\t\tif (header != null) {\n\t\t\tMiddlewareWebChromeBase tail = header;\n\t\t\tint count = 1;\n\t\t\tMiddlewareWebChromeBase tmp = header;\n\t\t\tfor (; tmp.next() != null; ) {\n\t\t\t\ttail = tmp = tmp.next();\n\t\t\t\tcount++;\n\t\t\t}\n\t\t\tLogUtils.i(TAG, \"MiddlewareWebClientBase middleware count:\" + count);\n\t\t\ttail.setDelegate(mDefaultChromeClient);\n\t\t\treturn this.mTargetChromeClient = header;\n\t\t} else {\n\t\t\treturn this.mTargetChromeClient = mDefaultChromeClient;\n\t\t}\n\t}\n\n\tpublic enum SecurityType {\n\t\tDEFAULT_CHECK, STRICT_CHECK;\n\t}\n\n\tpublic static final class AgentBuilder {\n\t\tprivate Activity mActivity;\n\t\tprivate Fragment mFragment;\n\t\tprivate ViewGroup mViewGroup;\n\t\tprivate boolean mIsNeedDefaultProgress;\n\t\tprivate int mIndex = -1;\n\t\tprivate BaseIndicatorView mBaseIndicatorView;\n\t\tprivate IndicatorController mIndicatorController = null;\n\t\t/*默认进度条是显示的*/\n\t\tprivate boolean mEnableIndicator = true;\n\t\tprivate ViewGroup.LayoutParams mLayoutParams = null;\n\t\tprivate com.just.agentweb.WebViewClient mWebViewClient;\n\t\tprivate com.just.agentweb.WebChromeClient mWebChromeClient;\n\t\tprivate int mIndicatorColor = -1;\n\t\tprivate IAgentWebSettings mAgentWebSettings;\n\t\tprivate WebCreator mWebCreator;\n\t\tprivate HttpHeaders mHttpHeaders = null;\n\t\tprivate IEventHandler mIEventHandler;\n\t\tprivate int mHeight = -1;\n\t\tprivate ArrayMap<String, Object> mJavaObject;\n\t\tprivate SecurityType mSecurityType = SecurityType.DEFAULT_CHECK;\n\t\tprivate WebView mWebView;\n\t\tprivate boolean mWebClientHelper = true;\n\t\tprivate IWebLayout mWebLayout = null;\n\t\tprivate PermissionInterceptor mPermissionInterceptor = null;\n\t\tprivate AbsAgentWebUIController mAgentWebUIController;\n\t\tprivate DefaultWebClient.OpenOtherPageWays mOpenOtherPage = null;\n\t\tprivate boolean mIsInterceptUnkownUrl = false;\n\t\tprivate MiddlewareWebClientBase mMiddlewareWebClientBaseHeader;\n\t\tprivate MiddlewareWebClientBase mMiddlewareWebClientBaseTail;\n\t\tprivate MiddlewareWebChromeBase mChromeMiddleWareHeader = null;\n\t\tprivate MiddlewareWebChromeBase mChromeMiddleWareTail = null;\n\t\tprivate View mErrorView;\n\t\tprivate int mErrorLayout;\n\t\tprivate int mReloadId;\n\t\tprivate int mTag = -1;\n\n\t\tpublic AgentBuilder(@NonNull Activity activity, @NonNull Fragment fragment) {\n\t\t\tmActivity = activity;\n\t\t\tmFragment = fragment;\n\t\t\tmTag = AgentWeb.FRAGMENT_TAG;\n\t\t}\n\n\t\tpublic AgentBuilder(@NonNull Activity activity) {\n\t\t\tmActivity = activity;\n\t\t\tmTag = AgentWeb.ACTIVITY_TAG;\n\t\t}\n\n\n\t\tpublic IndicatorBuilder setAgentWebParent(@NonNull ViewGroup v, @NonNull ViewGroup.LayoutParams lp) {\n\t\t\tthis.mViewGroup = v;\n\t\t\tthis.mLayoutParams = lp;\n\t\t\treturn new IndicatorBuilder(this);\n\t\t}\n\n\t\tpublic IndicatorBuilder setAgentWebParent(@NonNull ViewGroup v, int index, @NonNull ViewGroup.LayoutParams lp) {\n\t\t\tthis.mViewGroup = v;\n\t\t\tthis.mLayoutParams = lp;\n\t\t\tthis.mIndex = index;\n\t\t\treturn new IndicatorBuilder(this);\n\t\t}\n\n\t\tprivate PreAgentWeb buildAgentWeb() {\n\t\t\tif (mTag == AgentWeb.FRAGMENT_TAG && this.mViewGroup == null) {\n\t\t\t\tthrow new NullPointerException(\"ViewGroup is null,Please check your parameters .\");\n\t\t\t}\n\t\t\treturn new PreAgentWeb(HookManager.hookAgentWeb(new AgentWeb(this), this));\n\t\t}\n\n\t\tprivate void addJavaObject(String key, Object o) {\n\t\t\tif (mJavaObject == null) {\n\t\t\t\tmJavaObject = new ArrayMap<>();\n\t\t\t}\n\t\t\tmJavaObject.put(key, o);\n\t\t}\n\n\t\tprivate void addHeader(String baseUrl, String k, String v) {\n\t\t\tif (mHttpHeaders == null) {\n\t\t\t\tmHttpHeaders = HttpHeaders.create();\n\t\t\t}\n\t\t\tmHttpHeaders.additionalHttpHeader(baseUrl, k, v);\n\t\t}\n\n\t\tprivate void addHeader(String baseUrl, Map<String, String> headers) {\n\t\t\tif (mHttpHeaders == null) {\n\t\t\t\tmHttpHeaders = HttpHeaders.create();\n\t\t\t}\n\t\t\tmHttpHeaders.additionalHttpHeaders(baseUrl, headers);\n\t\t}\n\t}\n\n\tpublic static class IndicatorBuilder {\n\t\tprivate AgentBuilder mAgentBuilder = null;\n\n\t\tpublic IndicatorBuilder(AgentBuilder agentBuilder) {\n\t\t\tthis.mAgentBuilder = agentBuilder;\n\t\t}\n\n\t\tpublic CommonBuilder useDefaultIndicator(int color) {\n\t\t\tthis.mAgentBuilder.mEnableIndicator = true;\n\t\t\tthis.mAgentBuilder.mIndicatorColor = color;\n\t\t\treturn new CommonBuilder(mAgentBuilder);\n\t\t}\n\n\t\tpublic CommonBuilder useDefaultIndicator() {\n\t\t\tthis.mAgentBuilder.mEnableIndicator = true;\n\t\t\treturn new CommonBuilder(mAgentBuilder);\n\t\t}\n\n\t\tpublic CommonBuilder closeIndicator() {\n\t\t\tthis.mAgentBuilder.mEnableIndicator = false;\n\t\t\tthis.mAgentBuilder.mIndicatorColor = -1;\n\t\t\tthis.mAgentBuilder.mHeight = -1;\n\t\t\treturn new CommonBuilder(mAgentBuilder);\n\t\t}\n\n\t\tpublic CommonBuilder setCustomIndicator(@NonNull BaseIndicatorView v) {\n\t\t\tif (v != null) {\n\t\t\t\tthis.mAgentBuilder.mEnableIndicator = true;\n\t\t\t\tthis.mAgentBuilder.mBaseIndicatorView = v;\n\t\t\t\tthis.mAgentBuilder.mIsNeedDefaultProgress = false;\n\t\t\t} else {\n\t\t\t\tthis.mAgentBuilder.mEnableIndicator = true;\n\t\t\t\tthis.mAgentBuilder.mIsNeedDefaultProgress = true;\n\t\t\t}\n\t\t\treturn new CommonBuilder(mAgentBuilder);\n\t\t}\n\n\t\tpublic CommonBuilder useDefaultIndicator(@ColorInt int color, int height_dp) {\n\t\t\tthis.mAgentBuilder.mIndicatorColor = color;\n\t\t\tthis.mAgentBuilder.mHeight = height_dp;\n\t\t\treturn new CommonBuilder(this.mAgentBuilder);\n\t\t}\n\t}\n\n\tpublic static class CommonBuilder {\n\t\tprivate AgentBuilder mAgentBuilder;\n\n\t\tpublic CommonBuilder(AgentBuilder agentBuilder) {\n\t\t\tthis.mAgentBuilder = agentBuilder;\n\t\t}\n\n\t\tpublic CommonBuilder setEventHanadler(@Nullable IEventHandler iEventHandler) {\n\t\t\tmAgentBuilder.mIEventHandler = iEventHandler;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder closeWebViewClientHelper() {\n\t\t\tmAgentBuilder.mWebClientHelper = false;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder setWebChromeClient(@Nullable com.just.agentweb.WebChromeClient webChromeClient) {\n\t\t\tthis.mAgentBuilder.mWebChromeClient = webChromeClient;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder setWebViewClient(@Nullable com.just.agentweb.WebViewClient webChromeClient) {\n\t\t\tthis.mAgentBuilder.mWebViewClient = webChromeClient;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder useMiddlewareWebClient(@NonNull MiddlewareWebClientBase middleWrareWebClientBase) {\n\t\t\tif (middleWrareWebClientBase == null) {\n\t\t\t\treturn this;\n\t\t\t}\n\t\t\tif (this.mAgentBuilder.mMiddlewareWebClientBaseHeader == null) {\n\t\t\t\tthis.mAgentBuilder.mMiddlewareWebClientBaseHeader = this.mAgentBuilder.mMiddlewareWebClientBaseTail = middleWrareWebClientBase;\n\t\t\t} else {\n\t\t\t\tthis.mAgentBuilder.mMiddlewareWebClientBaseTail.enq(middleWrareWebClientBase);\n\t\t\t\tthis.mAgentBuilder.mMiddlewareWebClientBaseTail = middleWrareWebClientBase;\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder useMiddlewareWebChrome(@NonNull MiddlewareWebChromeBase middlewareWebChromeBase) {\n\t\t\tif (middlewareWebChromeBase == null) {\n\t\t\t\treturn this;\n\t\t\t}\n\t\t\tif (this.mAgentBuilder.mChromeMiddleWareHeader == null) {\n\t\t\t\tthis.mAgentBuilder.mChromeMiddleWareHeader = this.mAgentBuilder.mChromeMiddleWareTail = middlewareWebChromeBase;\n\t\t\t} else {\n\t\t\t\tthis.mAgentBuilder.mChromeMiddleWareTail.enq(middlewareWebChromeBase);\n\t\t\t\tthis.mAgentBuilder.mChromeMiddleWareTail = middlewareWebChromeBase;\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder setMainFrameErrorView(@NonNull View view) {\n\t\t\tthis.mAgentBuilder.mErrorView = view;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder setMainFrameErrorView(@LayoutRes int errorLayout, @IdRes int clickViewId) {\n\t\t\tthis.mAgentBuilder.mErrorLayout = errorLayout;\n\t\t\tthis.mAgentBuilder.mReloadId = clickViewId;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder setAgentWebWebSettings(@Nullable IAgentWebSettings agentWebSettings) {\n\t\t\tthis.mAgentBuilder.mAgentWebSettings = agentWebSettings;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic PreAgentWeb createAgentWeb() {\n\t\t\treturn this.mAgentBuilder.buildAgentWeb();\n\t\t}\n\n\n\t\tpublic CommonBuilder addJavascriptInterface(@NonNull String name, @NonNull Object o) {\n\t\t\tthis.mAgentBuilder.addJavaObject(name, o);\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder setSecurityType(@NonNull SecurityType type) {\n\t\t\tthis.mAgentBuilder.mSecurityType = type;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder setWebView(@Nullable WebView webView) {\n\t\t\tthis.mAgentBuilder.mWebView = webView;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder setWebLayout(@Nullable IWebLayout iWebLayout) {\n\t\t\tthis.mAgentBuilder.mWebLayout = iWebLayout;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder additionalHttpHeader(String baseUrl, String k, String v) {\n\t\t\tthis.mAgentBuilder.addHeader(baseUrl, k, v);\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder additionalHttpHeader(String baseUrl, Map<String, String> headers) {\n\t\t\tthis.mAgentBuilder.addHeader(baseUrl, headers);\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder setPermissionInterceptor(@Nullable PermissionInterceptor permissionInterceptor) {\n\t\t\tthis.mAgentBuilder.mPermissionInterceptor = permissionInterceptor;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder setAgentWebUIController(@Nullable AgentWebUIControllerImplBase agentWebUIController) {\n\t\t\tthis.mAgentBuilder.mAgentWebUIController = agentWebUIController;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder setOpenOtherPageWays(@Nullable DefaultWebClient.OpenOtherPageWays openOtherPageWays) {\n\t\t\tthis.mAgentBuilder.mOpenOtherPage = openOtherPageWays;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic CommonBuilder interceptUnkownUrl() {\n\t\t\tthis.mAgentBuilder.mIsInterceptUnkownUrl = true;\n\t\t\treturn this;\n\t\t}\n\t}\n\n\tprivate static final class PermissionInterceptorWrapper implements PermissionInterceptor {\n\n\t\tprivate WeakReference<PermissionInterceptor> mWeakReference;\n\n\t\tprivate PermissionInterceptorWrapper(PermissionInterceptor permissionInterceptor) {\n\t\t\tthis.mWeakReference = new WeakReference<PermissionInterceptor>(permissionInterceptor);\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean intercept(String url, String[] permissions, String a) {\n\t\t\tif (this.mWeakReference.get() == null) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn mWeakReference.get().intercept(url, permissions, a);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/AgentWebConfig.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.content.Context;\nimport android.os.AsyncTask;\nimport android.os.Build;\nimport android.text.TextUtils;\nimport android.webkit.CookieManager;\nimport android.webkit.CookieSyncManager;\nimport android.webkit.ValueCallback;\nimport android.webkit.WebView;\n\nimport androidx.annotation.Nullable;\n\nimport java.io.File;\n\nimport static com.just.agentweb.AgentWebUtils.getAgentWebFilePath;\n/**\n * @since 1.0.0\n * @author cenxiaozhong\n */\npublic class AgentWebConfig {\n\n\tstatic final String FILE_CACHE_PATH = \"agentweb-cache\";\n\tstatic final String AGENTWEB_CACHE_PATCH = File.separator + \"agentweb-cache\";\n\t/**\n\t * 缓存路径\n\t */\n\tstatic String AGENTWEB_FILE_PATH;\n\t/**\n\t * DEBUG 模式 ， 如果需要查看日志请设置为 true\n\t */\n\tpublic static boolean DEBUG = false;\n\t/**\n\t * 当前操作系统是否低于 KITKAT\n\t */\n\tstatic final boolean IS_KITKAT_OR_BELOW_KITKAT = Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT;\n\t/**\n\t * 默认 WebView  类型 。\n\t */\n\tpublic static final int WEBVIEW_DEFAULT_TYPE = 1;\n\t/**\n\t * 使用 AgentWebView\n\t */\n\tpublic static final int WEBVIEW_AGENTWEB_SAFE_TYPE = 2;\n\t/**\n\t * 自定义 WebView\n\t */\n\tpublic static final int WEBVIEW_CUSTOM_TYPE = 3;\n\tstatic int WEBVIEW_TYPE = WEBVIEW_DEFAULT_TYPE;\n\tprivate static volatile boolean IS_INITIALIZED = false;\n\tprivate static final String TAG = AgentWebConfig.class.getSimpleName();\n\t/**\n\t * AgentWeb 的版本\n\t */\n\tpublic static final String AGENTWEB_VERSION = \" agentweb/4.0.2 \";\n\tpublic static final String AGENTWEB_NAME=\"AgentWeb\";\n\t/**\n\t * 通过JS获取的文件大小， 这里限制最大为5MB ，太大会抛出 OutOfMemoryError\n\t */\n\tpublic static int MAX_FILE_LENGTH = 1024 * 1024 * 5;\n\t//获取Cookie\n\tpublic static String getCookiesByUrl(String url) {\n\t\treturn CookieManager.getInstance() == null ? null : CookieManager.getInstance().getCookie(url);\n\t}\n\n\tpublic static void debug() {\n\t\tDEBUG = true;\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n\t\t\tWebView.setWebContentsDebuggingEnabled(true);\n\t\t}\n\t}\n\t/**\n\t * 删除所有已经过期的 Cookies\n\t */\n\tpublic static void removeExpiredCookies() {\n\t\tCookieManager mCookieManager = null;\n\t\tif ((mCookieManager = CookieManager.getInstance()) != null) { //同步清除\n\t\t\tmCookieManager.removeExpiredCookie();\n\t\t\ttoSyncCookies();\n\t\t}\n\t}\n\t/**\n\t * 删除所有 Cookies\n\t */\n\tpublic static void removeAllCookies() {\n\t\tremoveAllCookies(null);\n\t}\n\n\t// 解决兼容 Android 4.4 java.lang.NoSuchMethodError: android.webkit.CookieManager.removeSessionCookies\n\tpublic static void removeSessionCookies() {\n\t\tremoveSessionCookies(null);\n\t}\n\t/**\n\t * 同步cookie\n\t *\n\t * @param url\n\t * @param cookies\n\t */\n\tpublic static void syncCookie(String url, String cookies) {\n\t\tCookieManager mCookieManager = CookieManager.getInstance();\n\t\tif (mCookieManager != null) {\n\t\t\tmCookieManager.setCookie(url, cookies);\n\t\t\ttoSyncCookies();\n\t\t}\n\t}\n\n\tpublic static void removeSessionCookies(ValueCallback<Boolean> callback) {\n\t\tif (callback == null) {\n\t\t\tcallback = getDefaultIgnoreCallback();\n\t\t}\n\t\tif (CookieManager.getInstance() == null) {\n\t\t\tcallback.onReceiveValue(new Boolean(false));\n\t\t\treturn;\n\t\t}\n\t\tif (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {\n\t\t\tCookieManager.getInstance().removeSessionCookie();\n\t\t\ttoSyncCookies();\n\t\t\tcallback.onReceiveValue(new Boolean(true));\n\t\t\treturn;\n\t\t}\n\t\tCookieManager.getInstance().removeSessionCookies(callback);\n\t\ttoSyncCookies();\n\t}\n\t/**\n\t * @param context\n\t * @return WebView 的缓存路径\n\t */\n\tpublic static String getCachePath(Context context) {\n\t\treturn context.getCacheDir().getAbsolutePath() + AGENTWEB_CACHE_PATCH;\n\t}\n\t/**\n\t * @param context\n\t * @return AgentWeb 缓存路径\n\t */\n\tpublic static String getExternalCachePath(Context context) {\n\t\treturn getAgentWebFilePath(context);\n\t}\n\n\n\t//Android  4.4  NoSuchMethodError: android.webkit.CookieManager.removeAllCookies\n\tpublic static void removeAllCookies(@Nullable ValueCallback<Boolean> callback) {\n\t\tif (callback == null) {\n\t\t\tcallback = getDefaultIgnoreCallback();\n\t\t}\n\t\tif (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {\n\t\t\tCookieManager.getInstance().removeAllCookie();\n\t\t\ttoSyncCookies();\n\t\t\tcallback.onReceiveValue(!CookieManager.getInstance().hasCookies());\n\t\t\treturn;\n\t\t}\n\t\tCookieManager.getInstance().removeAllCookies(callback);\n\t\ttoSyncCookies();\n\t}\n\n\t/**\n\t * 清空缓存\n\t *\n\t * @param context\n\t */\n\tpublic static synchronized void clearDiskCache(Context context) {\n\t\ttry {\n\t\t\tAgentWebUtils.clearCacheFolder(new File(getCachePath(context)), 0);\n\t\t\tString path = getExternalCachePath(context);\n\t\t\tif (!TextUtils.isEmpty(path)) {\n\t\t\t\tFile mFile = new File(path);\n\t\t\t\tAgentWebUtils.clearCacheFolder(mFile, 0);\n\t\t\t}\n\t\t} catch (Throwable throwable) {\n\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\tthrowable.printStackTrace();\n\t\t\t}\n\t\t}\n\t}\n\n\n\tstatic synchronized void initCookiesManager(Context context) {\n\t\tif (!IS_INITIALIZED) {\n\t\t\tcreateCookiesSyncInstance(context);\n\t\t\tIS_INITIALIZED = true;\n\t\t}\n\t}\n\n\tprivate static void createCookiesSyncInstance(Context context) {\n\t\tif (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {\n\t\t\tCookieSyncManager.createInstance(context);\n\t\t}\n\t}\n\n\tprivate static void toSyncCookies() {\n\t\tif (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {\n\t\t\tCookieSyncManager.getInstance().sync();\n\t\t\treturn;\n\t\t}\n\t\tAsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {\n\t\t\t@Override\n\t\t\tpublic void run() {\n\t\t\t\tCookieManager.getInstance().flush();\n\t\t\t}\n\t\t});\n\t}\n\n\tstatic String getDatabasesCachePath(Context context) {\n\t\treturn context.getApplicationContext().getDir(\"database\", Context.MODE_PRIVATE).getPath();\n\t}\n\n\tprivate static ValueCallback<Boolean> getDefaultIgnoreCallback() {\n\t\treturn new ValueCallback<Boolean>() {\n\t\t\t@Override\n\t\t\tpublic void onReceiveValue(Boolean ignore) {\n\t\t\t\tLogUtils.i(TAG, \"removeExpiredCookies:\" + ignore);\n\t\t\t}\n\t\t};\n\t}\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/AgentWebFileProvider.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.content.Context;\nimport android.content.pm.ProviderInfo;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.content.FileProvider;\n\n/**\n * @since 2.0.0\n * @author cenxiaozhong\n */\npublic class AgentWebFileProvider extends FileProvider {\n\n    @Override\n    public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {\n        super.attachInfo(context, info);\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/AgentWebJsInterfaceCompat.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.app.Activity;\nimport android.os.Handler;\nimport android.os.Message;\nimport android.webkit.JavascriptInterface;\n\nimport java.lang.ref.WeakReference;\n\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\npublic class AgentWebJsInterfaceCompat {\n\n\tprivate WeakReference<AgentWeb> mReference = null;\n\tprivate WeakReference<Activity> mActivityWeakReference = null;\n\tprivate String TAG = this.getClass().getSimpleName();\n\n\tAgentWebJsInterfaceCompat(AgentWeb agentWeb, Activity activity) {\n\t\tmReference = new WeakReference<AgentWeb>(agentWeb);\n\t\tmActivityWeakReference = new WeakReference<Activity>(activity);\n\t}\n\n\t@JavascriptInterface\n\tpublic void uploadFile() {\n\t\tuploadFile(\"*/*\");\n\t}\n\n\t@JavascriptInterface\n\tpublic void uploadFile(String acceptType) {\n\t\tLogUtils.i(TAG, acceptType + \"  \" + mActivityWeakReference.get() + \"  \" + mReference.get());\n\t\tif (mActivityWeakReference.get() != null && mReference.get() != null) {\n\t\t\tAgentWebUtils.showFileChooserCompat(mActivityWeakReference.get(),\n\t\t\t\t\tmReference.get().getWebCreator().getWebView(),\n\t\t\t\t\tnull,\n\t\t\t\t\tnull,\n\t\t\t\t\tmReference.get().getPermissionInterceptor(),\n\t\t\t\t\tnull,\n\t\t\t\t\tacceptType,\n\t\t\t\t\tnew Handler.Callback() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic boolean handleMessage(Message msg) {\n\t\t\t\t\t\t\tif (mReference.get() != null) {\n\t\t\t\t\t\t\t\tmReference.get().getJsAccessEntrace()\n\t\t\t\t\t\t\t\t\t\t.quickCallJs(\"uploadFileResult\",\n\t\t\t\t\t\t\t\t\t\t\t\tmsg.obj instanceof String ? (String) msg.obj : null);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/AgentWebPermissions.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.Manifest;\n\n\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\npublic class AgentWebPermissions {\n\tpublic static final String[] CAMERA;\n\tpublic static final String[] LOCATION;\n\tpublic static final String[] STORAGE;\n\tpublic static final String ACTION_CAMERA = \"Camera\";\n\tpublic static final String ACTION_LOCATION = \"Location\";\n\tpublic static final String ACTION_STORAGE = \"Storage\";\n\tstatic {\n\t\tCAMERA = new String[]{\n\t\t\t\tManifest.permission.CAMERA};\n\t\tLOCATION = new String[]{\n\t\t\t\tManifest.permission.ACCESS_FINE_LOCATION,\n\t\t\t\tManifest.permission.ACCESS_COARSE_LOCATION};\n\t\tSTORAGE = new String[]{\n\t\t\t\tManifest.permission.READ_EXTERNAL_STORAGE,\n\t\t\t\tManifest.permission.WRITE_EXTERNAL_STORAGE};\n\t}\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/AgentWebSettingsImpl.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.ContextWrapper;\nimport android.webkit.DownloadListener;\nimport android.webkit.WebView;\n\n\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\npublic class AgentWebSettingsImpl extends AbsAgentWebSettings {\n    private AgentWeb mAgentWeb;\n\n    @Override\n    protected void bindAgentWebSupport(AgentWeb agentWeb) {\n        this.mAgentWeb = agentWeb;\n    }\n\n    @Override\n    public WebListenerManager setDownloader(WebView webView, DownloadListener downloadListener) {\n        // Fix Android 5.1 crashing:\n        // ClassCastException: android.app.ContextImpl cannot be cast to android.app.Activity\n        if (downloadListener == null) {\n            Activity activity = getActivityByContext(webView.getContext());\n            downloadListener = DefaultDownloadImpl.create(activity, webView, mAgentWeb.getPermissionInterceptor());\n        }\n        return super.setDownloader(webView, downloadListener);\n    }\n\n    /**\n     * Copy from com.blankj.utilcode.util.ActivityUtils#getActivityByView\n     */\n    private Activity getActivityByContext(Context context) {\n        if (context instanceof Activity) return (Activity) context;\n        while (context instanceof ContextWrapper) {\n            if (context instanceof Activity) {\n                return (Activity) context;\n            }\n            context = ((ContextWrapper) context).getBaseContext();\n        }\n\n\n        LogUtils.e( \"获取 B :\", context + \"\" );\n        System.out.println(\"输出：\" + context + \"\");\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/AgentWebUIControllerImplBase.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.app.Activity;\nimport android.os.Handler;\nimport android.webkit.JsPromptResult;\nimport android.webkit.JsResult;\nimport android.webkit.WebView;\n\n\n/**\n * @author cenxiaozhong\n * @date 2017/12/6\n * @since 3.0.0\n */\npublic class AgentWebUIControllerImplBase extends AbsAgentWebUIController {\n\n\tpublic static AbsAgentWebUIController build() {\n\t\treturn new AgentWebUIControllerImplBase();\n\t}\n\n\t@Override\n\tpublic void onJsAlert(WebView view, String url, String message) {\n\t\tgetDelegate().onJsAlert(view, url, message);\n\t}\n\n\t@Override\n\tpublic void onOpenPagePrompt(WebView view, String url, Handler.Callback callback) {\n\t\tgetDelegate().onOpenPagePrompt(view, url, callback);\n\t}\n\n\t@Override\n\tpublic void onJsConfirm(WebView view, String url, String message, JsResult jsResult) {\n\t\tgetDelegate().onJsConfirm(view, url, message, jsResult);\n\t}\n\n\t@Override\n\tpublic void onSelectItemsPrompt(WebView view, String url, String[] ways, Handler.Callback callback) {\n\t\tgetDelegate().onSelectItemsPrompt(view, url, ways, callback);\n\t}\n\n\t@Override\n\tpublic void onForceDownloadAlert(String url, Handler.Callback callback) {\n\t\tgetDelegate().onForceDownloadAlert(url, callback);\n\t}\n\n\t@Override\n\tpublic void onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult jsPromptResult) {\n\t\tgetDelegate().onJsPrompt(view, url, message, defaultValue, jsPromptResult);\n\t}\n\n\t@Override\n\tpublic void onMainFrameError(WebView view, int errorCode, String description, String failingUrl) {\n\t\tgetDelegate().onMainFrameError(view, errorCode, description, failingUrl);\n\t}\n\n\t@Override\n\tpublic void onShowMainFrame() {\n\t\tgetDelegate().onShowMainFrame();\n\t}\n\n\t@Override\n\tpublic void onLoading(String msg) {\n\t\tgetDelegate().onLoading(msg);\n\t}\n\n\t@Override\n\tpublic void onCancelLoading() {\n\t\tgetDelegate().onCancelLoading();\n\t}\n\n\n\t@Override\n\tpublic void onShowMessage(String message, String from) {\n\t\tgetDelegate().onShowMessage(message, from);\n\t}\n\n\t@Override\n\tpublic void onPermissionsDeny(String[] permissions, String permissionType, String action) {\n\t\tgetDelegate().onPermissionsDeny(permissions, permissionType, action);\n\t}\n\n\t@Override\n\tprotected void bindSupportWebParent(WebParentLayout webParentLayout, Activity activity) {\n\t\tgetDelegate().bindSupportWebParent(webParentLayout, activity);\n\t}\n\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/AgentWebUtils.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.app.Activity;\nimport android.content.ContentUris;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.pm.ApplicationInfo;\nimport android.content.pm.PackageManager;\nimport android.content.pm.ResolveInfo;\nimport android.database.Cursor;\nimport android.net.ConnectivityManager;\nimport android.net.NetworkInfo;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Environment;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.StatFs;\nimport android.provider.DocumentsContract;\nimport android.provider.MediaStore;\nimport android.telephony.TelephonyManager;\nimport android.text.SpannableString;\nimport android.text.Spanned;\nimport android.text.TextUtils;\nimport android.text.format.DateUtils;\nimport android.text.style.ForegroundColorSpan;\nimport android.util.Log;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.ViewParent;\nimport android.webkit.ValueCallback;\nimport android.webkit.WebChromeClient;\nimport android.webkit.WebSettings;\nimport android.webkit.WebView;\nimport android.widget.Toast;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.NonNull;\nimport androidx.core.app.AppOpsManagerCompat;\nimport androidx.core.content.ContextCompat;\nimport androidx.core.content.FileProvider;\nimport androidx.core.os.EnvironmentCompat;\nimport androidx.loader.content.CursorLoader;\n\nimport com.google.android.material.snackbar.Snackbar;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.IOException;\nimport java.lang.ref.WeakReference;\nimport java.lang.reflect.Method;\nimport java.math.BigInteger;\nimport java.security.MessageDigest;\nimport java.text.SimpleDateFormat;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\n\nimport static com.just.agentweb.AgentWebConfig.AGENTWEB_FILE_PATH;\nimport static com.just.agentweb.AgentWebConfig.FILE_CACHE_PATH;\n\n\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\npublic class AgentWebUtils {\n\n\tprivate static final String TAG = AgentWebUtils.class.getSimpleName();\n\tprivate static Handler mHandler = null;\n\n\tprivate AgentWebUtils() {\n\t\tthrow new UnsupportedOperationException(\"u can't init me\");\n\t}\n\n\tpublic static int dp2px(Context context, float dipValue) {\n\t\tfinal float scale = context.getResources().getDisplayMetrics().density;\n\t\treturn (int) (dipValue * scale + 0.5f);\n\t}\n\n\tstatic final void clearWebView(WebView m) {\n\t\tif (m == null) {\n\t\t\treturn;\n\t\t}\n\t\tif (Looper.myLooper() != Looper.getMainLooper()) {\n\t\t\treturn;\n\t\t}\n\t\tm.loadUrl(\"about:blank\");\n\t\tm.stopLoading();\n\t\tif (m.getHandler() != null) {\n\t\t\tm.getHandler().removeCallbacksAndMessages(null);\n\t\t}\n\t\tm.removeAllViews();\n\t\tViewGroup mViewGroup = null;\n\t\tif ((mViewGroup = ((ViewGroup) m.getParent())) != null) {\n\t\t\tmViewGroup.removeView(m);\n\t\t}\n\t\tm.setWebChromeClient(null);\n\t\tm.setWebViewClient(null);\n\t\tm.setTag(null);\n\t\tm.clearHistory();\n\t\tm.destroy();\n\t\tm = null;\n\t}\n\n\tpublic static String getAgentWebFilePath(Context context) {\n\t\tif (!TextUtils.isEmpty(AGENTWEB_FILE_PATH)) {\n\t\t\treturn AGENTWEB_FILE_PATH;\n\t\t}\n\t\tString dir = getDiskExternalCacheDir(context);\n\t\tFile mFile = new File(dir, FILE_CACHE_PATH);\n\t\ttry {\n\t\t\tif (!mFile.exists()) {\n\t\t\t\tmFile.mkdirs();\n\t\t\t}\n\t\t} catch (Throwable throwable) {\n\t\t\tLogUtils.i(TAG, \"create dir exception\");\n\t\t}\n\t\tLogUtils.i(TAG, \"path:\" + mFile.getAbsolutePath() + \"  path:\" + mFile.getPath());\n\t\treturn AGENTWEB_FILE_PATH = mFile.getAbsolutePath();\n\t}\n\n\n\tpublic static File createFileByName(Context context, String name, boolean cover) throws IOException {\n\t\tString path = getAgentWebFilePath(context);\n\t\tif (TextUtils.isEmpty(path)) {\n\t\t\treturn null;\n\t\t}\n\t\tFile mFile = new File(path, name);\n\t\tif (mFile.exists()) {\n\t\t\tif (cover) {\n\t\t\t\tmFile.delete();\n\t\t\t\tmFile.createNewFile();\n\t\t\t}\n\t\t} else {\n\t\t\tmFile.createNewFile();\n\t\t}\n\t\treturn mFile;\n\t}\n\n\tpublic static int checkNetworkType(Context context) {\n\t\tint netType = 0;\n\t\t//连接管理对象\n\t\tConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);\n\t\t//获取NetworkInfo对象\n\t\t@SuppressLint(\"MissingPermission\") NetworkInfo networkInfo = manager.getActiveNetworkInfo();\n\t\tif (networkInfo == null) {\n\t\t\treturn netType;\n\t\t}\n\t\tswitch (networkInfo.getType()) {\n\t\t\tcase ConnectivityManager.TYPE_WIFI:\n\t\t\tcase ConnectivityManager.TYPE_WIMAX:\n\t\t\tcase ConnectivityManager.TYPE_ETHERNET:\n\t\t\t\treturn 1;\n\t\t\tcase ConnectivityManager.TYPE_MOBILE:\n\t\t\t\tswitch (networkInfo.getSubtype()) {\n\t\t\t\t\tcase TelephonyManager.NETWORK_TYPE_LTE:  // 4G\n\t\t\t\t\tcase TelephonyManager.NETWORK_TYPE_HSPAP:\n\t\t\t\t\tcase TelephonyManager.NETWORK_TYPE_EHRPD:\n\t\t\t\t\t\treturn 2;\n\t\t\t\t\tcase TelephonyManager.NETWORK_TYPE_UMTS: // 3G\n\t\t\t\t\tcase TelephonyManager.NETWORK_TYPE_CDMA:\n\t\t\t\t\tcase TelephonyManager.NETWORK_TYPE_EVDO_0:\n\t\t\t\t\tcase TelephonyManager.NETWORK_TYPE_EVDO_A:\n\t\t\t\t\tcase TelephonyManager.NETWORK_TYPE_EVDO_B:\n\t\t\t\t\t\treturn 3;\n\t\t\t\t\tcase TelephonyManager.NETWORK_TYPE_GPRS: // 2G\n\t\t\t\t\tcase TelephonyManager.NETWORK_TYPE_EDGE:\n\t\t\t\t\t\treturn 4;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn netType;\n\t\t\t\t}\n\n\t\t\tdefault:\n\t\t\t\treturn netType;\n\t\t}\n\t}\n\n\tpublic static long getAvailableStorage() {\n\t\ttry {\n\t\t\tStatFs stat = new StatFs(Environment.getExternalStorageDirectory().toString());\n\t\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {\n\t\t\t\treturn stat.getAvailableBlocksLong() * stat.getBlockSizeLong();\n\t\t\t} else {\n\t\t\t\treturn (long) stat.getAvailableBlocks() * (long) stat.getBlockSize();\n\t\t\t}\n\t\t} catch (RuntimeException ex) {\n\t\t\treturn 0;\n\t\t}\n\t}\n\n\n\tstatic Uri getUriFromFile(Context context, File file) {\n\t\tUri uri = null;\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n\t\t\turi = getUriFromFileForN(context, file);\n\t\t} else {\n\t\t\turi = Uri.fromFile(file);\n\t\t}\n\t\treturn uri;\n\t}\n\n\tstatic Uri getUriFromFileForN(Context context, File file) {\n\t\tUri fileUri = FileProvider.getUriForFile(context, context.getPackageName() + \".AgentWebFileProvider\", file);\n\t\treturn fileUri;\n\t}\n\n\n\tstatic void setIntentDataAndType(Context context,\n\t                                 Intent intent,\n\t                                 String type,\n\t                                 File file,\n\t                                 boolean writeAble) {\n\t\tif (Build.VERSION.SDK_INT >= 24) {\n\t\t\tintent.setDataAndType(getUriFromFile(context, file), type);\n\t\t\tintent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);\n\t\t\tif (writeAble) {\n\t\t\t\tintent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);\n\t\t\t}\n\t\t} else {\n\t\t\tintent.setDataAndType(Uri.fromFile(file), type);\n\t\t}\n\t}\n\n\n\tstatic void setIntentData(Context context,\n\t                          Intent intent,\n\t                          File file,\n\t                          boolean writeAble) {\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n\t\t\tintent.setData(getUriFromFile(context, file));\n\t\t\tintent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);\n\t\t\tif (writeAble) {\n\t\t\t\tintent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);\n\t\t\t}\n\t\t} else {\n\t\t\tintent.setData(Uri.fromFile(file));\n\t\t}\n\t}\n\n\tstatic String getDiskExternalCacheDir(Context context) {\n\t\tFile mFile = context.getExternalCacheDir();\n\t\tif (Environment.MEDIA_MOUNTED.equals(EnvironmentCompat.getStorageState(mFile))) {\n\t\t\treturn mFile.getAbsolutePath();\n\t\t}\n\t\treturn null;\n\t}\n\n\tstatic void grantPermissions(Context context, Intent intent, Uri uri, boolean writeAble) {\n\t\tint flag = Intent.FLAG_GRANT_READ_URI_PERMISSION;\n\t\tif (writeAble) {\n\t\t\tflag |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;\n\t\t}\n\t\tintent.addFlags(flag);\n\t\tList<ResolveInfo> resInfoList = context.getPackageManager()\n\t\t\t\t.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);\n\t\tfor (ResolveInfo resolveInfo : resInfoList) {\n\t\t\tString packageName = resolveInfo.activityInfo.packageName;\n\t\t\tcontext.grantUriPermission(packageName, uri, flag);\n\t\t}\n\t}\n\n\n\tprivate static String getMIMEType(File f) {\n\t\tString type = \"\";\n\t\tString fName = f.getName();\n\t\t/* 取得扩展名 */\n\t\tString end = fName.substring(fName.lastIndexOf(\".\") + 1, fName.length()).toLowerCase();\n\t\t/* 依扩展名的类型决定MimeType */\n\t\tif (end.equals(\"pdf\")) {\n\t\t\ttype = \"application/pdf\";//\n\t\t} else if (end.equals(\"m4a\") || end.equals(\"mp3\") || end.equals(\"mid\") ||\n\t\t\t\tend.equals(\"xmf\") || end.equals(\"ogg\") || end.equals(\"wav\")) {\n\t\t\ttype = \"audio/*\";\n\t\t} else if (end.equals(\"3gp\") || end.equals(\"mp4\")) {\n\t\t\ttype = \"video/*\";\n\t\t} else if (end.equals(\"jpg\") || end.equals(\"gif\") || end.equals(\"png\") ||\n\t\t\t\tend.equals(\"jpeg\") || end.equals(\"bmp\")) {\n\t\t\ttype = \"image/*\";\n\t\t} else if (end.equals(\"apk\")) {\n\t\t\ttype = \"application/vnd.android.package-archive\";\n\t\t} else if (end.equals(\"pptx\") || end.equals(\"ppt\")) {\n\t\t\ttype = \"application/vnd.ms-powerpoint\";\n\t\t} else if (end.equals(\"docx\") || end.equals(\"doc\")) {\n\t\t\ttype = \"application/vnd.ms-word\";\n\t\t} else if (end.equals(\"xlsx\") || end.equals(\"xls\")) {\n\t\t\ttype = \"application/vnd.ms-excel\";\n\t\t} else {\n\t\t\ttype = \"*/*\";\n\t\t}\n\t\treturn type;\n\t}\n\n\n\tprivate static WeakReference<Snackbar> snackbarWeakReference;\n\n\tstatic void show(View parent,\n\t                 CharSequence text,\n\t                 int duration,\n\t                 @ColorInt int textColor,\n\t                 @ColorInt int bgColor,\n\t                 CharSequence actionText,\n\t                 @ColorInt int actionTextColor,\n\t                 View.OnClickListener listener) {\n\t\tSpannableString spannableString = new SpannableString(text);\n\t\tForegroundColorSpan colorSpan = new ForegroundColorSpan(textColor);\n\t\tspannableString.setSpan(colorSpan, 0, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n\t\tsnackbarWeakReference = new WeakReference<>(Snackbar.make(parent, spannableString, duration));\n\t\tSnackbar snackbar = snackbarWeakReference.get();\n\t\tView view = snackbar.getView();\n\t\tview.setBackgroundColor(bgColor);\n\t\tif (actionText != null && actionText.length() > 0 && listener != null) {\n\t\t\tsnackbar.setActionTextColor(actionTextColor);\n\t\t\tsnackbar.setAction(actionText, listener);\n\t\t}\n\t\tsnackbar.show();\n\t}\n\n\tstatic void dismiss() {\n\t\tif (snackbarWeakReference != null && snackbarWeakReference.get() != null) {\n\t\t\tsnackbarWeakReference.get().dismiss();\n\t\t\tsnackbarWeakReference = null;\n\t\t}\n\t}\n\n\tpublic static boolean checkWifi(Context context) {\n\t\tConnectivityManager connectivity = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);\n\t\tif (connectivity == null) {\n\t\t\treturn false;\n\t\t}\n\t\t@SuppressLint(\"MissingPermission\") NetworkInfo info = connectivity.getActiveNetworkInfo();\n\t\treturn info != null && info.isConnected() && info.getType() == ConnectivityManager.TYPE_WIFI;\n\t}\n\n\tpublic static boolean checkNetwork(Context context) {\n\t\tConnectivityManager connectivity = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);\n\t\tif (connectivity == null) {\n\t\t\treturn false;\n\t\t}\n\t\t@SuppressLint(\"MissingPermission\") NetworkInfo info = connectivity.getActiveNetworkInfo();\n\t\treturn info != null && info.isConnected();\n\t}\n\n\tstatic boolean isOverriedMethod(Object currentObject, String methodName, String method, Class... clazzs) {\n\t\tLogUtils.i(TAG, \"  methodName:\" + methodName + \"   method:\" + method);\n\t\tboolean tag = false;\n\t\tif (currentObject == null) {\n\t\t\treturn tag;\n\t\t}\n\t\ttry {\n\t\t\tClass clazz = currentObject.getClass();\n\t\t\tMethod mMethod = clazz.getMethod(methodName, clazzs);\n\t\t\tString gStr = mMethod.toGenericString();\n\t\t\ttag = !gStr.contains(method);\n\t\t} catch (Exception igonre) {\n\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\tigonre.printStackTrace();\n\t\t\t}\n\t\t}\n\n\t\tLogUtils.i(TAG, \"isOverriedMethod:\" + tag);\n\t\treturn tag;\n\t}\n\n\tstatic Method isExistMethod(Object o, String methodName, Class... clazzs) {\n\n\t\tif (null == o) {\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\tClass clazz = o.getClass();\n\t\t\tMethod mMethod = clazz.getDeclaredMethod(methodName, clazzs);\n\t\t\tmMethod.setAccessible(true);\n\t\t\treturn mMethod;\n\t\t} catch (Throwable ignore) {\n\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\tignore.printStackTrace();\n\t\t\t}\n\t\t}\n\t\treturn null;\n\n\t}\n\n\tstatic void clearAgentWebCache(Context context) {\n\t\ttry {\n\t\t\tclearCacheFolder(new File(getAgentWebFilePath(context)), 0);\n\t\t} catch (Throwable throwable) {\n\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\tthrowable.printStackTrace();\n\t\t\t}\n\t\t}\n\t}\n\n\tstatic void clearWebViewAllCache(Context context, WebView webView) {\n\n\t\ttry {\n\n\t\t\tAgentWebConfig.removeAllCookies(null);\n\t\t\twebView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);\n\t\t\tcontext.deleteDatabase(\"webviewCache.db\");\n\t\t\tcontext.deleteDatabase(\"webview.db\");\n\t\t\twebView.clearCache(true);\n\t\t\twebView.clearHistory();\n\t\t\twebView.clearFormData();\n\t\t\tclearCacheFolder(new File(AgentWebConfig.getCachePath(context)), 0);\n\n\t\t} catch (Exception ignore) {\n\t\t\t//ignore.printStackTrace();\n\t\t\tif (AgentWebConfig.DEBUG) {\n\t\t\t\tignore.printStackTrace();\n\t\t\t}\n\t\t}\n\t}\n\n\tstatic void clearWebViewAllCache(Context context) {\n\n\t\ttry {\n\n\t\t\tclearWebViewAllCache(context, new LollipopFixedWebView(context.getApplicationContext()));\n\t\t} catch (Exception e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n\n\tstatic int clearCacheFolder(final File dir, final int numDays) {\n\t\tint deletedFiles = 0;\n\t\tif (dir != null) {\n\t\t\tLog.i(\"Info\", \"dir:\" + dir.getAbsolutePath());\n\t\t}\n\t\tif (dir != null && dir.isDirectory()) {\n\t\t\ttry {\n\t\t\t\tfor (File child : dir.listFiles()) {\n\n\t\t\t\t\t//first delete subdirectories recursively\n\t\t\t\t\tif (child.isDirectory()) {\n\t\t\t\t\t\tdeletedFiles += clearCacheFolder(child, numDays);\n\t\t\t\t\t}\n\n\t\t\t\t\t//then delete the files and subdirectories in this dir\n\t\t\t\t\t//only empty directories can be deleted, so subdirs have been done first\n\t\t\t\t\tif (child.lastModified() < new Date().getTime() - numDays * DateUtils.DAY_IN_MILLIS) {\n\t\t\t\t\t\tLog.i(TAG, \"file name:\" + child.getName());\n\t\t\t\t\t\tif (child.delete()) {\n\t\t\t\t\t\t\tdeletedFiles++;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (Exception e) {\n\t\t\t\tLog.e(\"Info\", String.format(\"Failed to clean the cache, result %s\", e.getMessage()));\n\t\t\t}\n\t\t}\n\t\treturn deletedFiles;\n\t}\n\n\n\tstatic void clearCache(final Context context, final int numDays) {\n\t\tLog.i(\"Info\", String.format(\"Starting cache prune, deleting files older than %d days\", numDays));\n\t\tint numDeletedFiles = clearCacheFolder(context.getCacheDir(), numDays);\n\t\tLog.i(\"Info\", String.format(\"Cache pruning completed, %d files deleted\", numDeletedFiles));\n\t}\n\n\tpublic static String[] uriToPath(Activity activity, Uri[] uris) {\n\t\tif (activity == null || uris == null || uris.length == 0) {\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\tString[] paths = new String[uris.length];\n\t\t\tint i = 0;\n\t\t\tfor (Uri mUri : uris) {\n\t\t\t\tpaths[i++] = Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 ? getFileAbsolutePath(activity, mUri) : getRealPathBelowVersion(activity, mUri);\n\n\t\t\t}\n\t\t\treturn paths;\n\t\t} catch (Throwable throwable) {\n\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\tthrowable.printStackTrace();\n\t\t\t}\n\t\t}\n\t\treturn null;\n\n\t}\n\n\tprivate static String getRealPathBelowVersion(Context context, Uri uri) {\n\t\tString filePath = null;\n\t\tLogUtils.i(TAG, \"method -> getRealPathBelowVersion \" + uri + \"   path:\" + uri.getPath() + \"    getAuthority:\" + uri.getAuthority());\n\t\tString[] projection = {MediaStore.Images.Media.DATA};\n\t\tCursorLoader loader = new CursorLoader(context, uri, projection, null,\n\t\t\t\tnull, null);\n\t\tCursor cursor = loader.loadInBackground();\n\t\tif (cursor != null) {\n\t\t\tcursor.moveToFirst();\n\t\t\tfilePath = cursor.getString(cursor.getColumnIndex(projection[0]));\n\t\t\tcursor.close();\n\t\t}\n\t\tif (filePath == null) {\n\t\t\tfilePath = uri.getPath();\n\t\t}\n\t\treturn filePath;\n\t}\n\n\tstatic File createImageFile(Context context) {\n\t\tFile mFile = null;\n\t\ttry {\n\t\t\tString timeStamp =\n\t\t\t\t\tnew SimpleDateFormat(\"yyyyMMddHHmmss\", Locale.getDefault()).format(new Date());\n\t\t\tString imageName = String.format(\"aw_%s.jpg\", timeStamp);\n\t\t\tmFile = createFileByName(context, imageName, true);\n\t\t} catch (Throwable e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t\treturn mFile;\n\t}\n\n\tstatic File createVideoFile(Context context){\n\t\tFile mFile = null;\n\t\ttry {\n\t\t\tString timeStamp =\n\t\t\t\t\tnew SimpleDateFormat(\"yyyyMMddHHmmss\", Locale.getDefault()).format(new Date());\n\t\t\tString imageName = String.format(\"aw_%s.mp4\", timeStamp);  //默认生成mp4\n\t\t\tmFile = createFileByName(context, imageName, true);\n\t\t} catch (Throwable e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t\treturn mFile;\n\t}\n\n\n\tpublic static void closeIO(Closeable closeable) {\n\t\ttry {\n\t\t\tif (closeable != null) {\n\t\t\t\tcloseable.close();\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n\n\t@TargetApi(19)\n\tstatic String getFileAbsolutePath(Activity context, Uri fileUri) {\n\t\tif (context == null || fileUri == null) {\n\t\t\treturn null;\n\t\t}\n\t\tLogUtils.i(TAG, \"getAuthority:\" + fileUri.getAuthority() + \"  getHost:\" + fileUri.getHost() + \"   getPath:\" + fileUri.getPath() + \"  getScheme:\" + fileUri.getScheme() + \"  query:\" + fileUri.getQuery());\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, fileUri)) {\n\t\t\tif (isExternalStorageDocument(fileUri)) {\n\t\t\t\tString docId = DocumentsContract.getDocumentId(fileUri);\n\t\t\t\tString[] split = docId.split(\":\");\n\t\t\t\tString type = split[0];\n\t\t\t\tif (\"primary\".equalsIgnoreCase(type)) {\n\t\t\t\t\treturn Environment.getExternalStorageDirectory() + \"/\" + split[1];\n\t\t\t\t}\n\t\t\t} else if (isDownloadsDocument(fileUri)) {\n\t\t\t\tString id = DocumentsContract.getDocumentId(fileUri);\n\t\t\t\tUri contentUri = ContentUris.withAppendedId(Uri.parse(\"content://downloads/public_downloads\"), Long.valueOf(id));\n\t\t\t\treturn getDataColumn(context, contentUri, null, null);\n\t\t\t} else if (isMediaDocument(fileUri)) {\n\t\t\t\tString docId = DocumentsContract.getDocumentId(fileUri);\n\t\t\t\tString[] split = docId.split(\":\");\n\t\t\t\tString type = split[0];\n\n\t\t\t\tUri contentUri = null;\n\t\t\t\tif (\"image\".equals(type)) {\n\t\t\t\t\tcontentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;\n\t\t\t\t} else if (\"video\".equals(type)) {\n\t\t\t\t\tcontentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;\n\t\t\t\t} else if (\"audio\".equals(type)) {\n\t\t\t\t\tcontentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;\n\t\t\t\t}\n\t\t\t\tString selection = MediaStore.Images.Media._ID + \"=?\";\n\t\t\t\tString[] selectionArgs = new String[]{split[1]};\n\t\t\t\treturn getDataColumn(context, contentUri, selection, selectionArgs);\n\t\t\t} else {\n\t\t\t}\n\t\t} // MediaStore (and general)\n\t\telse if (fileUri.getAuthority().equalsIgnoreCase(context.getPackageName() + \".AgentWebFileProvider\")) {\n\t\t\tString path = fileUri.getPath();\n\t\t\tint index = path.lastIndexOf(\"/\");\n\t\t\treturn getAgentWebFilePath(context) + File.separator + path.substring(index + 1, path.length());\n\t\t} else if (\"content\".equalsIgnoreCase(fileUri.getScheme())) {\n\t\t\t// Return the remote address\n\t\t\tif (isGooglePhotosUri(fileUri)) {\n\t\t\t\treturn fileUri.getLastPathSegment();\n\t\t\t}\n\t\t\treturn getDataColumn(context, fileUri, null, null);\n\t\t}\n\t\t// File\n\t\telse if (\"file\".equalsIgnoreCase(fileUri.getScheme())) {\n\t\t\treturn fileUri.getPath();\n\t\t}\n\t\treturn null;\n\t}\n\n\tstatic String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {\n\t\tCursor cursor = null;\n\t\tString[] projection = {MediaStore.Images.Media.DATA};\n\t\ttry {\n\t\t\tcursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);\n\t\t\tif (cursor != null && cursor.moveToFirst()) {\n\t\t\t\tint index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);\n\t\t\t\treturn cursor.getString(index);\n\t\t\t}\n\t\t} finally {\n\t\t\tif (cursor != null) {\n\t\t\t\tcursor.close();\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * @param uri The Uri to check.\n\t * @return Whether the Uri authority is ExternalStorageProvider.\n\t */\n\tstatic boolean isExternalStorageDocument(Uri uri) {\n\t\treturn \"com.android.externalstorage.documents\".equals(uri.getAuthority());\n\t}\n\n\t/**\n\t * @param uri The Uri to check.\n\t * @return Whether the Uri authority is DownloadsProvider.\n\t */\n\tstatic boolean isDownloadsDocument(Uri uri) {\n\t\treturn \"com.android.providers.downloads.documents\".equals(uri.getAuthority());\n\t}\n\n\t/**\n\t * @param uri The Uri to check.\n\t * @return Whether the Uri authority is MediaProvider.\n\t */\n\tstatic boolean isMediaDocument(Uri uri) {\n\t\treturn \"com.android.providers.media.documents\".equals(uri.getAuthority());\n\t}\n\n\t/**\n\t * @param uri The Uri to check.\n\t * @return Whether the Uri authority is Google Photos.\n\t */\n\tstatic boolean isGooglePhotosUri(Uri uri) {\n\t\treturn \"com.google.android.apps.photos.content\".equals(uri.getAuthority());\n\t}\n\n\tstatic Intent getInstallApkIntentCompat(Context context, File file) {\n\t\tIntent mIntent = new Intent().setAction(Intent.ACTION_VIEW);\n\t\tsetIntentDataAndType(context, mIntent, \"application/vnd.android.package-archive\", file, false);\n\t\treturn mIntent;\n\t}\n\n\tpublic static Intent getCommonFileIntentCompat(Context context, File file) {\n\t\tIntent mIntent = new Intent().setAction(Intent.ACTION_VIEW);\n\t\tsetIntentDataAndType(context, mIntent, getMIMEType(file), file, false);\n\t\treturn mIntent;\n\t}\n\n\tstatic Intent getIntentCaptureCompat(Context context, File file) {\n\t\tIntent mIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);\n\t\tUri mUri = getUriFromFile(context, file);\n\t\tmIntent.addCategory(Intent.CATEGORY_DEFAULT);\n\t\tmIntent.putExtra(MediaStore.EXTRA_OUTPUT, mUri);\n\t\treturn mIntent;\n\t}\n\n\tstatic Intent getIntentVideoCompat(Context context, File file){\n\t\tIntent mIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);\n\t\tUri mUri = getUriFromFile(context, file);\n\t\tmIntent.addCategory(Intent.CATEGORY_DEFAULT);\n\t\tmIntent.putExtra(MediaStore.EXTRA_OUTPUT, mUri);\n\t\treturn mIntent;\n\t}\n\n\n\tstatic boolean isJson(String target) {\n\t\tif (TextUtils.isEmpty(target)) {\n\t\t\treturn false;\n\t\t}\n\t\tboolean tag = false;\n\t\ttry {\n\t\t\tif (target.startsWith(\"[\")) {\n\t\t\t\tnew JSONArray(target);\n\t\t\t} else {\n\t\t\t\tnew JSONObject(target);\n\t\t\t}\n\t\t\ttag = true;\n\t\t} catch (JSONException ignore) {\n//            ignore.printStackTrace();\n\t\t\ttag = false;\n\t\t}\n\t\treturn tag;\n\t}\n\n\tpublic static boolean isUIThread() {\n\t\treturn Looper.myLooper() == Looper.getMainLooper();\n\t}\n\n\tstatic boolean isEmptyCollection(Collection collection) {\n\t\treturn collection == null || collection.isEmpty();\n\t}\n\n\tstatic boolean isEmptyMap(Map map) {\n\t\treturn map == null || map.isEmpty();\n\t}\n\n\tprivate static Toast mToast = null;\n\tstatic void toastShowShort(Context context, String msg) {\n\t\tif (mToast == null) {\n\t\t\tmToast = Toast.makeText(context.getApplicationContext(), msg, Toast.LENGTH_SHORT);\n\t\t} else {\n\t\t\tmToast.setText(msg);\n\t\t}\n\t\tmToast.show();\n\t}\n\n\t@Deprecated\n\tstatic void getUIControllerAndShowMessage(Activity activity, String message, String from) {\n\t\tif (activity == null || activity.isFinishing()) {\n\t\t\treturn;\n\t\t}\n\t\tWebParentLayout mWebParentLayout = (WebParentLayout) activity.findViewById(R.id.web_parent_layout_id);\n\t\tAbsAgentWebUIController mAgentWebUIController = mWebParentLayout.provide();\n\t\tif (mAgentWebUIController != null) {\n\t\t\tmAgentWebUIController.onShowMessage(message, from);\n\t\t}\n\t}\n\n\tpublic static boolean hasPermission(@NonNull Context context, @NonNull String... permissions) {\n\t\treturn hasPermission(context, Arrays.asList(permissions));\n\t}\n\n\tpublic static boolean hasPermission(@NonNull Context context, @NonNull List<String> permissions) {\n\t\tif (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {\n\t\t\treturn true;\n\t\t}\n\t\tfor (String permission : permissions) {\n\t\t\tint result = ContextCompat.checkSelfPermission(context, permission);\n\t\t\tif (result == PackageManager.PERMISSION_DENIED) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tString op = AppOpsManagerCompat.permissionToOp(permission);\n\t\t\tif (TextUtils.isEmpty(op)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tresult = AppOpsManagerCompat.noteProxyOp(context, op, context.getPackageName());\n\t\t\tif (result != AppOpsManagerCompat.MODE_ALLOWED) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\tpublic static List<String> getDeniedPermissions(Activity activity, String[] permissions) {\n\t\tif (permissions == null || permissions.length == 0) {\n\t\t\treturn null;\n\t\t}\n\t\tList<String> deniedPermissions = new ArrayList<>();\n\t\tfor (int i = 0; i < permissions.length; i++) {\n\t\t\tif (!hasPermission(activity, permissions[i])) {\n\t\t\t\tdeniedPermissions.add(permissions[i]);\n\t\t\t}\n\t\t}\n\t\treturn deniedPermissions;\n\t}\n\n\n\tpublic static AbsAgentWebUIController getAgentWebUIControllerByWebView(WebView webView) {\n\t\tWebParentLayout mWebParentLayout = getWebParentLayoutByWebView(webView);\n\t\treturn mWebParentLayout.provide();\n\t}\n\n\t//获取应用的名称\n\tpublic static String getApplicationName(Context context) {\n\t\tPackageManager packageManager = null;\n\t\tApplicationInfo applicationInfo = null;\n\t\ttry {\n\t\t\tpackageManager = context.getApplicationContext().getPackageManager();\n\t\t\tapplicationInfo = packageManager.getApplicationInfo(context.getPackageName(), 0);\n\t\t} catch (PackageManager.NameNotFoundException e) {\n\t\t\tapplicationInfo = null;\n\t\t}\n\t\tString applicationName =\n\t\t\t\t(String) packageManager.getApplicationLabel(applicationInfo);\n\t\treturn applicationName;\n\t}\n\n\tstatic WebParentLayout getWebParentLayoutByWebView(WebView webView) {\n\t\tViewGroup mViewGroup = null;\n\t\tif (!(webView.getParent() instanceof ViewGroup)) {\n\t\t\tthrow new IllegalStateException(\"please check webcreator's create method was be called ?\");\n\t\t}\n\t\tmViewGroup = (ViewGroup) webView.getParent();\n\t\tAbsAgentWebUIController mAgentWebUIController;\n\t\twhile (mViewGroup != null) {\n\n\t\t\tLogUtils.i(TAG, \"ViewGroup:\" + mViewGroup);\n\t\t\tif (mViewGroup.getId() == R.id.web_parent_layout_id) {\n\t\t\t\tWebParentLayout mWebParentLayout = (WebParentLayout) mViewGroup;\n\t\t\t\tLogUtils.i(TAG, \"found WebParentLayout\");\n\t\t\t\treturn mWebParentLayout;\n\t\t\t} else {\n\t\t\t\tViewParent mViewParent = mViewGroup.getParent();\n\t\t\t\tif (mViewParent instanceof ViewGroup) {\n\t\t\t\t\tmViewGroup = (ViewGroup) mViewParent;\n\t\t\t\t} else {\n\t\t\t\t\tmViewGroup = null;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthrow new IllegalStateException(\"please check webcreator's create method was be called ?\");\n\t}\n\n\tpublic static void runInUiThread(Runnable runnable) {\n\t\tif (mHandler == null) {\n\t\t\tmHandler = new Handler(Looper.getMainLooper());\n\t\t}\n\t\tmHandler.post(runnable);\n\t}\n\n\tstatic boolean showFileChooserCompat(Activity activity,\n\t                                     WebView webView,\n\t                                     ValueCallback<Uri[]> valueCallbacks,\n\t                                     WebChromeClient.FileChooserParams fileChooserParams,\n\t                                     PermissionInterceptor permissionInterceptor,\n\t                                     ValueCallback valueCallback,\n\t                                     String mimeType,\n\t                                     Handler.Callback jsChannelCallback\n\t) {\n\t\ttry {\n\t\t\tClass<?> clz = Class.forName(\"com.just.agentweb.filechooser.FileChooser\");\n\t\t\tObject mFileChooser$Builder = clz.getDeclaredMethod(\"newBuilder\",\n\t\t\t\t\tActivity.class, WebView.class)\n\t\t\t\t\t.invoke(null, activity, webView);\n\t\t\tclz = mFileChooser$Builder.getClass();\n\t\t\tMethod mMethod = null;\n\t\t\tif (valueCallbacks != null) {\n\t\t\t\tmMethod = clz.getDeclaredMethod(\"setUriValueCallbacks\", ValueCallback.class);\n\t\t\t\tmMethod.setAccessible(true);\n\t\t\t\tmMethod.invoke(mFileChooser$Builder, valueCallbacks);\n\t\t\t}\n\t\t\tif (fileChooserParams != null) {\n\t\t\t\tmMethod = clz.getDeclaredMethod(\"setFileChooserParams\", WebChromeClient.FileChooserParams.class);\n\t\t\t\tmMethod.setAccessible(true);\n\t\t\t\tmMethod.invoke(mFileChooser$Builder, fileChooserParams);\n\t\t\t}\n\t\t\tif (valueCallback != null) {\n\t\t\t\tmMethod = clz.getDeclaredMethod(\"setUriValueCallback\", ValueCallback.class);\n\t\t\t\tmMethod.setAccessible(true);\n\t\t\t\tmMethod.invoke(mFileChooser$Builder, valueCallback);\n\t\t\t}\n\t\t\tif (!TextUtils.isEmpty(mimeType)) {\n//                LogUtils.i(TAG, Arrays.toString(clz.getDeclaredMethods()));\n\t\t\t\tmMethod = clz.getDeclaredMethod(\"setAcceptType\", String.class);\n\t\t\t\tmMethod.setAccessible(true);\n\t\t\t\tmMethod.invoke(mFileChooser$Builder, mimeType);\n\t\t\t}\n\t\t\tif (jsChannelCallback != null) {\n\t\t\t\tmMethod = clz.getDeclaredMethod(\"setJsChannelCallback\", Handler.Callback.class);\n\t\t\t\tmMethod.setAccessible(true);\n\t\t\t\tmMethod.invoke(mFileChooser$Builder, jsChannelCallback);\n\t\t\t}\n\t\t\tmMethod = clz.getDeclaredMethod(\"setPermissionInterceptor\", PermissionInterceptor.class);\n\t\t\tmMethod.setAccessible(true);\n\t\t\tmMethod.invoke(mFileChooser$Builder, permissionInterceptor);\n\t\t\tmMethod = clz.getDeclaredMethod(\"build\");\n\t\t\tmMethod.setAccessible(true);\n\t\t\tObject mFileChooser = mMethod.invoke(mFileChooser$Builder);\n\t\t\tmMethod = mFileChooser.getClass().getDeclaredMethod(\"openFileChooser\");\n\t\t\tmMethod.setAccessible(true);\n\t\t\tmMethod.invoke(mFileChooser);\n\t\t} catch (Throwable throwable) {\n\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\tthrowable.printStackTrace();\n\t\t\t}\n\t\t\tif (throwable instanceof ClassNotFoundException) {\n\t\t\t\tLogUtils.e(TAG, \"Please check whether compile'com.just.agentweb:filechooser:x.x.x' dependency was added.\");\n\t\t\t}\n\t\t\tif (valueCallbacks != null) {\n\t\t\t\tLogUtils.i(TAG, \"onReceiveValue empty\");\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (valueCallback != null) {\n\t\t\t\tvalueCallback.onReceiveValue(null);\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\tpublic static String md5(String str) {\n\t\ttry {\n\t\t\tMessageDigest md = MessageDigest.getInstance(\"MD5\");\n\t\t\tmd.update(str.getBytes());\n\t\t\treturn new BigInteger(1, md.digest()).toString(16);\n\t\t} catch (Exception e) {\n\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\te.printStackTrace();\n\t\t\t}\n\t\t}\n\t\treturn \"\";\n\t}\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/AgentWebView.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.util.Pair;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.ViewParent;\nimport android.view.accessibility.AccessibilityManager;\nimport android.webkit.JsPromptResult;\nimport android.webkit.WebBackForwardList;\nimport android.webkit.WebChromeClient;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\nimport android.widget.Toast;\n\nimport org.json.JSONObject;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.util.HashMap;\nimport java.util.Map;\n\n\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\npublic class AgentWebView extends LollipopFixedWebView {\n    private static final String TAG = AgentWebView.class.getSimpleName();\n    private Map<String, JsCallJava> mJsCallJavas;\n    private Map<String, String> mInjectJavaScripts;\n    private FixedOnReceivedTitle mFixedOnReceivedTitle;\n    private boolean mIsInited;\n    private Boolean mIsAccessibilityEnabledOriginal;\n\n    public AgentWebView(Context context) {\n        this(context, null);\n    }\n\n    public AgentWebView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        removeSearchBoxJavaBridge();\n        mIsInited = true;\n        mFixedOnReceivedTitle = new FixedOnReceivedTitle();\n    }\n\n    /**\n     * 经过大量的测试，按照以下方式才能保证JS脚本100%注入成功：\n     * 1、在第一次loadUrl之前注入JS（在addJavascriptInterface里面注入即可，setWebViewClient和setWebChromeClient要在addJavascriptInterface之前执行）；\n     * 2、在webViewClient.onPageStarted中都注入JS；\n     * 3、在webChromeClient.onProgressChanged中都注入JS，并且不能通过自检查（onJsPrompt里面判断）JS是否注入成功来减少注入JS的次数，因为网页中的JS可以同时打开多个url导致无法控制检查的准确性；\n     *\n     * @deprecated Android 4.2.2及以上版本的 addJavascriptInterface 方法已经解决了安全问题，如果不使用“网页能将JS函数传到Java层”功能，不建议使用该类，毕竟系统的JS注入效率才是最高的；\n     */\n    @Override\n    @Deprecated\n    public final void addJavascriptInterface(Object interfaceObj, String interfaceName) {\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {\n            super.addJavascriptInterface(interfaceObj, interfaceName);\n            Log.i(TAG, \"注入\");\n            return;\n        } else {\n            Log.i(TAG, \"use mJsCallJavas:\" + interfaceName);\n        }\n\n        LogUtils.i(TAG, \"addJavascriptInterface:\" + interfaceObj + \"   interfaceName:\" + interfaceName);\n        if (mJsCallJavas == null) {\n            mJsCallJavas = new HashMap<String, JsCallJava>();\n        }\n        mJsCallJavas.put(interfaceName, new JsCallJava(interfaceObj, interfaceName));\n        injectJavaScript();\n        if (LogUtils.isDebug()) {\n            Log.d(TAG, \"injectJavaScript, addJavascriptInterface.interfaceObj = \" + interfaceObj + \", interfaceName = \" + interfaceName);\n        }\n        addJavascriptInterfaceSupport(interfaceObj, interfaceName);\n    }\n\n    protected void addJavascriptInterfaceSupport(Object interfaceObj, String interfaceName) {\n    }\n\n    @Override\n    public final void setWebChromeClient(WebChromeClient client) {\n        AgentWebChrome mAgentWebChrome = new AgentWebChrome(this);\n        mAgentWebChrome.setDelegate(client);\n        mFixedOnReceivedTitle.setWebChromeClient(client);\n        super.setWebChromeClient(mAgentWebChrome);\n        setWebChromeClientSupport(mAgentWebChrome);\n    }\n\n    protected final void setWebChromeClientSupport(WebChromeClient client) {\n    }\n\n    @Override\n    public final void setWebViewClient(WebViewClient client) {\n        AgentWebClient mAgentWebClient = new AgentWebClient(this);\n        mAgentWebClient.setDelegate(client);\n        super.setWebViewClient(mAgentWebClient);\n        setWebViewClientSupport(mAgentWebClient);\n    }\n\n    public final void setWebViewClientSupport(WebViewClient client) {\n    }\n\n    @Override\n    public void destroy() {\n        setVisibility(View.GONE);\n        if (mJsCallJavas != null) {\n            mJsCallJavas.clear();\n        }\n        if (mInjectJavaScripts != null) {\n            mInjectJavaScripts.clear();\n        }\n        removeAllViewsInLayout();\n        fixedStillAttached();\n        releaseConfigCallback();\n        if (mIsInited) {\n            resetAccessibilityEnabled();\n            LogUtils.i(TAG, \"destroy web\");\n            super.destroy();\n        }\n    }\n\n    @Override\n    public void clearHistory() {\n        if (mIsInited) {\n            super.clearHistory();\n        }\n    }\n\n    public static Pair<Boolean, String> isWebViewPackageException(Throwable e) {\n        String messageCause = e.getCause() == null ? e.toString() : e.getCause().toString();\n        String trace = Log.getStackTraceString(e);\n        if (trace.contains(\"android.content.pm.PackageManager$NameNotFoundException\")\n                || trace.contains(\"java.lang.RuntimeException: Cannot load WebView\")\n                || trace.contains(\"android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed\")) {\n\n            LogUtils.safeCheckCrash(TAG, \"isWebViewPackageException\", e);\n            return new Pair<Boolean, String>(true, \"WebView load failed, \" + messageCause);\n        }\n        return new Pair<Boolean, String>(false, messageCause);\n    }\n\n    @Override\n    public void setOverScrollMode(int mode) {\n        try {\n            super.setOverScrollMode(mode);\n        } catch (Throwable e) {\n            Pair<Boolean, String> pair = isWebViewPackageException(e);\n            if (pair.first) {\n                Toast.makeText(getContext(), pair.second, Toast.LENGTH_SHORT).show();\n                destroy();\n            } else {\n                throw e;\n            }\n        }\n    }\n\n    @Override\n    public boolean isPrivateBrowsingEnabled() {\n        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1\n                && getSettings() == null) {\n\n            return false; // getSettings().isPrivateBrowsingEnabled()\n        } else {\n            return super.isPrivateBrowsingEnabled();\n        }\n    }\n\n    /**\n     * 添加并注入JavaScript脚本（和“addJavascriptInterface”注入对象的注入时机一致，100%能注入成功）；\n     * 注意：为了做到能100%注入，需要在注入的js中自行判断对象是否已经存在（如：if (typeof(window.Android) = 'undefined')）；\n     *\n     * @param javaScript\n     */\n    public void addInjectJavaScript(String javaScript) {\n        if (mInjectJavaScripts == null) {\n            mInjectJavaScripts = new HashMap<String, String>();\n        }\n        mInjectJavaScripts.put(String.valueOf(javaScript.hashCode()), javaScript);\n        injectExtraJavaScript();\n    }\n\n    private void injectJavaScript() {\n        for (Map.Entry<String, JsCallJava> entry : mJsCallJavas.entrySet()) {\n            this.loadUrl(buildNotRepeatInjectJS(entry.getKey(), entry.getValue().getPreloadInterfaceJs()));\n        }\n    }\n\n    private void injectExtraJavaScript() {\n        for (Map.Entry<String, String> entry : mInjectJavaScripts.entrySet()) {\n            this.loadUrl(buildNotRepeatInjectJS(entry.getKey(), entry.getValue()));\n        }\n    }\n\n    /**\n     * 构建一个“不会重复注入”的js脚本；\n     *\n     * @param key\n     * @param js\n     * @return\n     */\n    public String buildNotRepeatInjectJS(String key, String js) {\n        String obj = String.format(\"__injectFlag_%1$s__\", key);\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"javascript:try{(function(){if(window.\");\n        sb.append(obj);\n        sb.append(\"){console.log('\");\n        sb.append(obj);\n        sb.append(\" has been injected');return;}window.\");\n        sb.append(obj);\n        sb.append(\"=true;\");\n        sb.append(js);\n        sb.append(\"}())}catch(e){console.warn(e)}\");\n        return sb.toString();\n    }\n\n    /**\n     * 构建一个“带try catch”的js脚本；\n     *\n     * @param js\n     * @return\n     */\n    public String buildTryCatchInjectJS(String js) {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"javascript:try{\");\n        sb.append(js);\n        sb.append(\"}catch(e){console.warn(e)}\");\n        return sb.toString();\n    }\n\n\n    public static class AgentWebClient extends MiddlewareWebClientBase {\n\n        private AgentWebView mAgentWebView;\n\n        private AgentWebClient(AgentWebView agentWebView) {\n            this.mAgentWebView = agentWebView;\n        }\n\n\n        @Override\n        public void onPageStarted(WebView view, String url, Bitmap favicon) {\n            super.onPageStarted(view, url, favicon);\n            if (mAgentWebView.mJsCallJavas != null) {\n                mAgentWebView.injectJavaScript();\n                if (LogUtils.isDebug()) {\n                    Log.d(TAG, \"injectJavaScript, onPageStarted.url = \" + view.getUrl());\n                }\n            }\n            if (mAgentWebView.mInjectJavaScripts != null) {\n                mAgentWebView.injectExtraJavaScript();\n            }\n            mAgentWebView.mFixedOnReceivedTitle.onPageStarted();\n            mAgentWebView.fixedAccessibilityInjectorExceptionForOnPageFinished(url);\n        }\n\n        @Override\n        public void onPageFinished(WebView view, String url) {\n            super.onPageFinished(view, url);\n            mAgentWebView.mFixedOnReceivedTitle.onPageFinished(view);\n            if (LogUtils.isDebug()) {\n                Log.d(TAG, \"onPageFinished.url = \" + view.getUrl());\n            }\n        }\n\n\n    }\n\n    public static class AgentWebChrome extends MiddlewareWebChromeBase {\n\n        private AgentWebView mAgentWebView;\n\n        private AgentWebChrome(AgentWebView agentWebView) {\n            this.mAgentWebView = agentWebView;\n        }\n\n        @Override\n        public void onReceivedTitle(WebView view, String title) {\n            this.mAgentWebView.mFixedOnReceivedTitle.onReceivedTitle();\n            super.onReceivedTitle(view, title);\n        }\n\n        @Override\n        public void onProgressChanged(WebView view, int newProgress) {\n            if (this.mAgentWebView.mJsCallJavas != null) {\n                this.mAgentWebView.injectJavaScript();\n                if (LogUtils.isDebug()) {\n                    Log.d(TAG, \"injectJavaScript, onProgressChanged.newProgress = \" + newProgress + \", url = \" + view.getUrl());\n                }\n            }\n            if (this.mAgentWebView.mInjectJavaScripts != null) {\n                this.mAgentWebView.injectExtraJavaScript();\n            }\n            super.onProgressChanged(view, newProgress);\n\n        }\n\n        @Override\n        public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {\n            Log.i(TAG, \"onJsPrompt:\" + url + \"  message:\" + message + \"  d:\" + defaultValue + \"  \");\n            if (this.mAgentWebView.mJsCallJavas != null && JsCallJava.isSafeWebViewCallMsg(message)) {\n                JSONObject jsonObject = JsCallJava.getMsgJSONObject(message);\n                String interfacedName = JsCallJava.getInterfacedName(jsonObject);\n                if (interfacedName != null) {\n                    JsCallJava mJsCallJava = this.mAgentWebView.mJsCallJavas.get(interfacedName);\n                    if (mJsCallJava != null) {\n                        result.confirm(mJsCallJava.call(view, jsonObject));\n                    }\n                }\n                return true;\n            } else {\n                return super.onJsPrompt(view, url, message, defaultValue, result);\n            }\n        }\n    }\n\n    /**\n     * 解决部分手机webView返回时不触发onReceivedTitle的问题（如：三星SM-G9008V 4.4.2）；\n     */\n    private static class FixedOnReceivedTitle {\n        private WebChromeClient mWebChromeClient;\n        private boolean mIsOnReceivedTitle;\n\n        public void setWebChromeClient(WebChromeClient webChromeClient) {\n            mWebChromeClient = webChromeClient;\n        }\n\n        public void onPageStarted() {\n            mIsOnReceivedTitle = false;\n        }\n\n        public void onPageFinished(WebView view) {\n            if (!mIsOnReceivedTitle && mWebChromeClient != null) {\n                WebBackForwardList list = null;\n                try {\n                    list = view.copyBackForwardList();\n                } catch (NullPointerException e) {\n                    if (LogUtils.isDebug()) {\n                        e.printStackTrace();\n                    }\n                }\n                if (list != null\n                        && list.getSize() > 0\n                        && list.getCurrentIndex() >= 0\n                        && list.getItemAtIndex(list.getCurrentIndex()) != null) {\n                    String previousTitle = list.getItemAtIndex(list.getCurrentIndex()).getTitle();\n                    mWebChromeClient.onReceivedTitle(view, previousTitle);\n                }\n            }\n        }\n        public void onReceivedTitle() {\n            mIsOnReceivedTitle = true;\n        }\n    }\n\n    // Activity在onDestory时调用webView的destroy，可以停止播放页面中的音频\n    private void fixedStillAttached() {\n        // java.lang.Throwable: Error: WebView.destroy() called while still attached!\n        // at android.webkit.WebViewClassic.destroy(WebViewClassic.java:4142)\n        // at android.webkit.WebView.destroy(WebView.java:707)\n        ViewParent parent = getParent();\n        if (parent instanceof ViewGroup) { // 由于自定义webView构建时传入了该Activity的context对象，因此需要先从父容器中移除webView，然后再销毁webView；\n            ViewGroup mWebViewContainer = (ViewGroup) getParent();\n            mWebViewContainer.removeAllViewsInLayout();\n        }\n    }\n\n    // 解决WebView内存泄漏问题；\n    private void releaseConfigCallback() {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { // JELLY_BEAN\n            try {\n                Field field = WebView.class.getDeclaredField(\"mWebViewCore\");\n                field = field.getType().getDeclaredField(\"mBrowserFrame\");\n                field = field.getType().getDeclaredField(\"sConfigCallback\");\n                field.setAccessible(true);\n                field.set(null, null);\n            } catch (NoSuchFieldException e) {\n                if (LogUtils.isDebug()) {\n                    e.printStackTrace();\n                }\n            } catch (IllegalAccessException e) {\n                if (LogUtils.isDebug()) {\n                    e.printStackTrace();\n                }\n            }\n        } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { // KITKAT\n            try {\n                Field sConfigCallback = Class.forName(\"android.webkit.BrowserFrame\").getDeclaredField(\"sConfigCallback\");\n                if (sConfigCallback != null) {\n                    sConfigCallback.setAccessible(true);\n                    sConfigCallback.set(null, null);\n                }\n            } catch (NoSuchFieldException e) {\n                if (LogUtils.isDebug()) {\n                    e.printStackTrace();\n                }\n            } catch (ClassNotFoundException e) {\n                if (LogUtils.isDebug()) {\n                    e.printStackTrace();\n                }\n            } catch (IllegalAccessException e) {\n                if (LogUtils.isDebug()) {\n                    e.printStackTrace();\n                }\n            }\n        }\n    }\n\n    /**\n     * Android 4.4 KitKat 使用Chrome DevTools 远程调试WebView\n     * WebView.setWebContentsDebuggingEnabled(true);\n     * http://blog.csdn.net/t12x3456/article/details/14225235\n     */\n    @TargetApi(19)\n    protected void trySetWebDebuggEnabled() {\n        if (LogUtils.isDebug() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n            try {\n                Class<?> clazz = WebView.class;\n                Method method = clazz.getMethod(\"setWebContentsDebuggingEnabled\", boolean.class);\n                method.invoke(null, true);\n            } catch (Throwable e) {\n                if (LogUtils.isDebug()) {\n                    e.printStackTrace();\n                }\n            }\n        }\n    }\n\n    @TargetApi(11)\n    protected boolean removeSearchBoxJavaBridge() {\n        try {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB\n                    && Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {\n                Method method = this.getClass().getMethod(\"removeJavascriptInterface\", String.class);\n                method.invoke(this, \"searchBoxJavaBridge_\");\n                return true;\n            }\n        } catch (Exception e) {\n            if (LogUtils.isDebug()) {\n                e.printStackTrace();\n            }\n        }\n        return false;\n    }\n\n    protected void fixedAccessibilityInjectorException() {\n        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1\n                && mIsAccessibilityEnabledOriginal == null\n                && isAccessibilityEnabled()) {\n            mIsAccessibilityEnabledOriginal = true;\n            setAccessibilityEnabled(false);\n        }\n    }\n\n    protected void fixedAccessibilityInjectorExceptionForOnPageFinished(String url) {\n        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN\n                && getSettings().getJavaScriptEnabled()\n                && mIsAccessibilityEnabledOriginal == null\n                && isAccessibilityEnabled()) {\n            try {\n                try {\n                    URLEncoder.encode(String.valueOf(new URI(url)), \"utf-8\");\n//                    URLEncodedUtils.parse(new URI(url), null); // AccessibilityInjector.getAxsUrlParameterValue\n                } catch (IllegalArgumentException e) {\n                    if (\"bad parameter\".equals(e.getMessage())) {\n                        mIsAccessibilityEnabledOriginal = true;\n                        setAccessibilityEnabled(false);\n                        LogUtils.safeCheckCrash(TAG, \"fixedAccessibilityInjectorExceptionForOnPageFinished.url = \" + url, e);\n                    }\n                }\n            } catch (Throwable e) {\n                if (LogUtils.isDebug()) {\n                    LogUtils.e(TAG, \"fixedAccessibilityInjectorExceptionForOnPageFinished\", e);\n                }\n            }\n        }\n    }\n\n    private boolean isAccessibilityEnabled() {\n        AccessibilityManager am = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);\n        return am.isEnabled();\n    }\n\n    private void setAccessibilityEnabled(boolean enabled) {\n        AccessibilityManager am = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);\n        try {\n            Method setAccessibilityState = am.getClass().getDeclaredMethod(\"setAccessibilityState\", boolean.class);\n            setAccessibilityState.setAccessible(true);\n            setAccessibilityState.invoke(am, enabled);\n            setAccessibilityState.setAccessible(false);\n        } catch (Throwable e) {\n            if (LogUtils.isDebug()) {\n                LogUtils.e(TAG, \"setAccessibilityEnabled\", e);\n            }\n        }\n    }\n\n    private void resetAccessibilityEnabled() {\n        if (mIsAccessibilityEnabledOriginal != null) {\n            setAccessibilityEnabled(mIsAccessibilityEnabledOriginal);\n        }\n    }\n}"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/BaseIndicatorSpec.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\n\n\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\npublic interface BaseIndicatorSpec {\n\n    void show();\n\n    void hide();\n\n    void reset();\n\n    void setProgress(int newProgress);\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/BaseIndicatorView.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.widget.FrameLayout;\n\nimport androidx.annotation.Nullable;\n\n\n/**\n * @author cenxiaozhong\n * @date 2017/5/12\n * @since 1.0.0\n */\npublic abstract class BaseIndicatorView extends FrameLayout implements BaseIndicatorSpec,LayoutParamsOffer{\n    public BaseIndicatorView(Context context) {\n        super(context);\n    }\n\n    public BaseIndicatorView(Context context, @Nullable AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public BaseIndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n    }\n\n    @Override\n    public void reset() {\n    }\n\n    @Override\n    public void setProgress(int newProgress) {\n    }\n\n    @Override\n    public void show() {\n    }\n\n    @Override\n    public void hide() {\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/BaseJsAccessEntrace.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.os.Build;\nimport android.webkit.ValueCallback;\nimport android.webkit.WebView;\n\n/**\n * @author cenxiaozhong\n * @date 2017/5/26\n * @since 1.0.0\n */\npublic abstract class BaseJsAccessEntrace implements JsAccessEntrace {\n    private WebView mWebView;\n    public static final String TAG=BaseJsAccessEntrace.class.getSimpleName();\n    BaseJsAccessEntrace(WebView webView){\n        this.mWebView=webView;\n    }\n    @Override\n    public void callJs(String js, final ValueCallback<String> callback) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n            this.evaluateJs(js, callback);\n        } else {\n            this.loadJs(js);\n        }\n    }\n    @Override\n    public void callJs(String js) {\n        this.callJs(js,  null);\n    }\n\n    private void loadJs(String js) {\n        mWebView.loadUrl(js);\n    }\n    private void evaluateJs(String js, final ValueCallback<String>callback){\n        mWebView.evaluateJavascript(js, new ValueCallback<String>() {\n            @Override\n            public void onReceiveValue(String value) {\n                if (callback != null){\n                    callback.onReceiveValue(value);\n                }\n            }\n        });\n    }\n\n\n    @Override\n    public void quickCallJs(String method, ValueCallback<String> callback, String... params) {\n        StringBuilder sb=new StringBuilder();\n        sb.append(\"javascript:\"+method);\n        if(params==null||params.length==0){\n            sb.append(\"()\");\n        }else{\n            sb.append(\"(\").append(concat(params)).append(\")\");\n        }\n        callJs(sb.toString(),callback);\n    }\n\n    private String concat(String...params){\n        StringBuilder mStringBuilder=new StringBuilder();\n        for(int i=0;i<params.length;i++){\n            String param=params[i];\n            if(!AgentWebUtils.isJson(param)){\n                mStringBuilder.append(\"\\\"\").append(param).append(\"\\\"\");\n            }else{\n                mStringBuilder.append(param);\n            }\n            if(i!=params.length-1){\n                mStringBuilder.append(\" , \");\n            }\n        }\n        return mStringBuilder.toString();\n    }\n\n    @Override\n    public void quickCallJs(String method, String... params) {\n        this.quickCallJs(method,null,params);\n    }\n\n    @Override\n    public void quickCallJs(String method) {\n        this.quickCallJs(method,(String[])null);\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/DefaultChromeClient.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.app.Activity;\nimport android.graphics.Bitmap;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.util.Log;\nimport android.view.View;\nimport android.webkit.ConsoleMessage;\nimport android.webkit.GeolocationPermissions;\nimport android.webkit.JsPromptResult;\nimport android.webkit.JsResult;\nimport android.webkit.ValueCallback;\nimport android.webkit.WebChromeClient;\nimport android.webkit.WebStorage;\nimport android.webkit.WebView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RequiresApi;\n\nimport java.lang.ref.WeakReference;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport static com.just.agentweb.ActionActivity.KEY_FROM_INTENTION;\n\n\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\npublic class DefaultChromeClient extends MiddlewareWebChromeBase {\n\t/**\n\t * Activity\n\t */\n\tprivate WeakReference<Activity> mActivityWeakReference = null;\n\t/**\n\t * DefaultChromeClient 's TAG\n\t */\n\tprivate String TAG = DefaultChromeClient.class.getSimpleName();\n\t/**\n\t * Android WebChromeClient path ，用于反射，用户是否重写来该方法\n\t */\n\tpublic static final String ANDROID_WEBCHROMECLIENT_PATH = \"android.webkit.WebChromeClient\";\n\t/**\n\t * WebChromeClient\n\t */\n\tprivate WebChromeClient mWebChromeClient;\n\t/**\n\t * 包装Flag\n\t */\n\tprivate boolean mIsWrapper = false;\n\t/**\n\t * Video 处理类\n\t */\n\tprivate IVideo mIVideo;\n\t/**\n\t * PermissionInterceptor 权限拦截器\n\t */\n\tprivate PermissionInterceptor mPermissionInterceptor;\n\t/**\n\t * 当前 WebView\n\t */\n\tprivate WebView mWebView;\n\t/**\n\t * Web端触发的定位 mOrigin\n\t */\n\tprivate String mOrigin = null;\n\t/**\n\t * Web 端触发的定位 Callback 回调成功，或者失败\n\t */\n\tprivate GeolocationPermissions.Callback mCallback = null;\n\t/**\n\t * 标志位\n\t */\n\tpublic static final int FROM_CODE_INTENTION = 0x18;\n\t/**\n\t * 标识当前是获取定位权限\n\t */\n\tpublic static final int FROM_CODE_INTENTION_LOCATION = FROM_CODE_INTENTION << 2;\n\t/**\n\t * AbsAgentWebUIController\n\t */\n\tprivate WeakReference<AbsAgentWebUIController> mAgentWebUIController = null;\n\t/**\n\t * IndicatorController 进度条控制器\n\t */\n\tprivate IndicatorController mIndicatorController;\n\t/**\n\t * 文件选择器\n\t */\n\tprivate Object mFileChooser;\n\n\tDefaultChromeClient(Activity activity,\n\t                    IndicatorController indicatorController,\n\t                    WebChromeClient chromeClient,\n\t                    @Nullable IVideo iVideo,\n\t                    PermissionInterceptor permissionInterceptor, WebView webView) {\n\t\tsuper(chromeClient);\n\t\tthis.mIndicatorController = indicatorController;\n\t\tmIsWrapper = chromeClient != null ? true : false;\n\t\tthis.mWebChromeClient = chromeClient;\n\t\tmActivityWeakReference = new WeakReference<Activity>(activity);\n\t\tthis.mIVideo = iVideo;\n\t\tthis.mPermissionInterceptor = permissionInterceptor;\n\t\tthis.mWebView = webView;\n\t\tmAgentWebUIController = new WeakReference<AbsAgentWebUIController>(AgentWebUtils.getAgentWebUIControllerByWebView(webView));\n\t}\n\n\n\t@Override\n\tpublic void onProgressChanged(WebView view, int newProgress) {\n\t\tsuper.onProgressChanged(view, newProgress);\n\t\tif (mIndicatorController != null) {\n\t\t\tmIndicatorController.progress(view, newProgress);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void onReceivedTitle(WebView view, String title) {\n\t\tif (mIsWrapper) {\n\t\t\tsuper.onReceivedTitle(view, title);\n\t\t}\n\t}\n\n\t@Override\n\tpublic boolean onJsAlert(WebView view, String url, String message, JsResult result) {\n\t\tif (mAgentWebUIController.get() != null) {\n\t\t\tmAgentWebUIController.get().onJsAlert(view, url, message);\n\t\t}\n\t\tresult.confirm();\n\t\treturn true;\n\t}\n\n\n\t@Override\n\tpublic void onReceivedIcon(WebView view, Bitmap icon) {\n\t\tsuper.onReceivedIcon(view, icon);\n\t}\n\n\t@Override\n\tpublic void onGeolocationPermissionsHidePrompt() {\n\t\tsuper.onGeolocationPermissionsHidePrompt();\n\t}\n\n\t//location\n\t@Override\n\tpublic void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {\n\t\tonGeolocationPermissionsShowPromptInternal(origin, callback);\n\t}\n\n\tprivate void onGeolocationPermissionsShowPromptInternal(String origin, GeolocationPermissions.Callback callback) {\n\t\tif (mPermissionInterceptor != null) {\n\t\t\tif (mPermissionInterceptor.intercept(this.mWebView.getUrl(), AgentWebPermissions.LOCATION, \"location\")) {\n\t\t\t\tcallback.invoke(origin, false, false);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tActivity mActivity = mActivityWeakReference.get();\n\t\tif (mActivity == null) {\n\t\t\tcallback.invoke(origin, false, false);\n\t\t\treturn;\n\t\t}\n\t\tList<String> deniedPermissions = null;\n\t\tif ((deniedPermissions = AgentWebUtils.getDeniedPermissions(mActivity, AgentWebPermissions.LOCATION)).isEmpty()) {\n\t\t\tLogUtils.i(TAG, \"onGeolocationPermissionsShowPromptInternal:\" + true);\n\t\t\tcallback.invoke(origin, true, false);\n\t\t} else {\n\t\t\tAction mAction = Action.createPermissionsAction(deniedPermissions.toArray(new String[]{}));\n\t\t\tmAction.setFromIntention(FROM_CODE_INTENTION_LOCATION);\n\t\t\tActionActivity.setPermissionListener(mPermissionListener);\n\t\t\tthis.mCallback = callback;\n\t\t\tthis.mOrigin = origin;\n\t\t\tActionActivity.start(mActivity, mAction);\n\t\t}\n\t}\n\n\tprivate ActionActivity.PermissionListener mPermissionListener = new ActionActivity.PermissionListener() {\n\t\t@Override\n\t\tpublic void onRequestPermissionsResult(@NonNull String[] permissions, @NonNull int[] grantResults, Bundle extras) {\n\t\t\tif (extras.getInt(KEY_FROM_INTENTION) == FROM_CODE_INTENTION_LOCATION) {\n\t\t\t\tboolean hasPermission = AgentWebUtils.hasPermission(mActivityWeakReference.get(), permissions);\n\t\t\t\tif (mCallback != null) {\n\t\t\t\t\tif (hasPermission) {\n\t\t\t\t\t\tmCallback.invoke(mOrigin, true, false);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmCallback.invoke(mOrigin, false, false);\n\t\t\t\t\t}\n\t\t\t\t\tmCallback = null;\n\t\t\t\t\tmOrigin = null;\n\t\t\t\t}\n\t\t\t\tif (!hasPermission && null != mAgentWebUIController.get()) {\n\t\t\t\t\tmAgentWebUIController\n\t\t\t\t\t\t\t.get()\n\t\t\t\t\t\t\t.onPermissionsDeny(\n\t\t\t\t\t\t\t\t\tAgentWebPermissions.LOCATION,\n\t\t\t\t\t\t\t\t\tAgentWebPermissions.ACTION_LOCATION,\n\t\t\t\t\t\t\t\t\t\"Location\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\t@Override\n\tpublic boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {\n\t\ttry {\n\t\t\tif (this.mAgentWebUIController.get() != null) {\n\t\t\t\tthis.mAgentWebUIController.get().onJsPrompt(mWebView, url, message, defaultValue, result);\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\te.printStackTrace();\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t@Override\n\tpublic boolean onJsConfirm(WebView view, String url, String message, JsResult result) {\n\t\tif (mAgentWebUIController.get() != null) {\n\t\t\tmAgentWebUIController.get().onJsConfirm(view, url, message, result);\n\t\t}\n\t\treturn true;\n\t}\n\n\n\t@Override\n\tpublic void onExceededDatabaseQuota(String url, String databaseIdentifier, long quota, long estimatedDatabaseSize, long totalQuota, WebStorage.QuotaUpdater quotaUpdater) {\n\t\tquotaUpdater.updateQuota(totalQuota * 2);\n\t}\n\n\t@Override\n\tpublic void onReachedMaxAppCacheSize(long requiredStorage, long quota, WebStorage.QuotaUpdater quotaUpdater) {\n\t\tquotaUpdater.updateQuota(requiredStorage * 2);\n\t}\n\n\t@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n\t@Override\n\tpublic boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {\n\t\tLogUtils.i(TAG, \"openFileChooser>=5.0\");\n\t\treturn openFileChooserAboveL(webView, filePathCallback, fileChooserParams);\n\t}\n\n\t@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n\tprivate boolean openFileChooserAboveL(WebView webView, ValueCallback<Uri[]> valueCallbacks, FileChooserParams fileChooserParams) {\n\t\tLogUtils.i(TAG, \"fileChooserParams:\" + fileChooserParams.getAcceptTypes() + \"  getTitle:\" + fileChooserParams.getTitle() + \" accept:\" + Arrays.toString(fileChooserParams.getAcceptTypes()) + \" length:\" + fileChooserParams.getAcceptTypes().length + \"  :\" + fileChooserParams.isCaptureEnabled() + \"  \" + fileChooserParams.getFilenameHint() + \"  intent:\" + fileChooserParams.createIntent().toString() + \"   mode:\" + fileChooserParams.getMode());\n\t\tActivity mActivity = this.mActivityWeakReference.get();\n\t\tif (mActivity == null || mActivity.isFinishing()) {\n\t\t\treturn false;\n\t\t}\n\t\treturn AgentWebUtils.showFileChooserCompat(mActivity,\n\t\t\t\tmWebView,\n\t\t\t\tvalueCallbacks,\n\t\t\t\tfileChooserParams,\n\t\t\t\tthis.mPermissionInterceptor,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tnull\n\t\t);\n\t}\n\n\t/**\n\t * Android  >= 4.1\n\t *\n\t * @param uploadFile ValueCallback ,  File URI callback\n\t * @param acceptType\n\t * @param capture\n\t */\n\t@Override\n\tpublic void openFileChooser(ValueCallback<Uri> uploadFile, String acceptType, String capture) {\n\t    /*believe me , i never want to do this */\n\t\tLogUtils.i(TAG, \"openFileChooser>=4.1\");\n\t\tcreateAndOpenCommonFileChooser(uploadFile, acceptType);\n\t}\n\n\t//  Android < 3.0\n\t@Override\n\tpublic void openFileChooser(ValueCallback<Uri> valueCallback) {\n\t\tLog.i(TAG, \"openFileChooser<3.0\");\n\t\tcreateAndOpenCommonFileChooser(valueCallback, \"*/*\");\n\t}\n\n\t//  Android  >= 3.0\n\t@Override\n\tpublic void openFileChooser(ValueCallback valueCallback, String acceptType) {\n\t\tLog.i(TAG, \"openFileChooser>3.0\");\n\t\tcreateAndOpenCommonFileChooser(valueCallback, acceptType);\n\t}\n\n\n\tprivate void createAndOpenCommonFileChooser(ValueCallback valueCallback, String mimeType) {\n\t\tActivity mActivity = this.mActivityWeakReference.get();\n\t\tif (mActivity == null || mActivity.isFinishing()) {\n\t\t\tvalueCallback.onReceiveValue(new Object());\n\t\t\treturn;\n\t\t}\n\t\tAgentWebUtils.showFileChooserCompat(mActivity,\n\t\t\t\tmWebView,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tthis.mPermissionInterceptor,\n\t\t\t\tvalueCallback,\n\t\t\t\tmimeType,\n\t\t\t\tnull\n\t\t);\n\t}\n\n\t@Override\n\tpublic boolean onConsoleMessage(ConsoleMessage consoleMessage) {\n\t\tsuper.onConsoleMessage(consoleMessage);\n\t\treturn true;\n\t}\n\n\t@Override\n\tpublic void onShowCustomView(View view, CustomViewCallback callback) {\n\t\tif (mIVideo != null) {\n\t\t\tmIVideo.onShowCustomView(view, callback);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void onHideCustomView() {\n\t\tif (mIVideo != null) {\n\t\t\tmIVideo.onHideCustomView();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/DefaultDesignUIController.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.app.Activity;\nimport android.content.DialogInterface;\nimport android.graphics.Color;\nimport android.os.Build;\nimport android.os.Handler;\nimport android.os.Message;\nimport android.text.TextUtils;\nimport android.util.TypedValue;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.webkit.JsPromptResult;\nimport android.webkit.JsResult;\nimport android.webkit.WebView;\nimport android.widget.TextView;\n\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.google.android.material.bottomsheet.BottomSheetDialog;\nimport com.google.android.material.snackbar.Snackbar;\n\n/**\n * @author cenxiaozhong\n * @date 2017/12/8\n * @since 3.0.0\n */\npublic class DefaultDesignUIController extends DefaultUIController {\n\n    private BottomSheetDialog mBottomSheetDialog;\n    private static final int RECYCLERVIEW_ID = 0x1001;\n    private Activity mActivity = null;\n    private WebParentLayout mWebParentLayout;\n    private LayoutInflater mLayoutInflater;\n\n    @Override\n    public void onJsAlert(WebView view, String url, String message) {\n        onJsAlertInternal(view, message);\n    }\n\n    private void onJsAlertInternal(WebView view, String message) {\n        Activity mActivity = this.mActivity;\n        if (mActivity == null || mActivity.isFinishing()) {\n            return;\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {\n            if (mActivity.isDestroyed()) {\n                return;\n            }\n        }\n        try {\n            AgentWebUtils.show(view,\n                    message,\n                    Snackbar.LENGTH_SHORT,\n                    Color.WHITE,\n                    mActivity.getResources().getColor(R.color.black),\n                    null,\n                    -1,\n                    null);\n        } catch (Throwable throwable) {\n            if (LogUtils.isDebug()){\n                throwable.printStackTrace();\n            }\n        }\n    }\n\n    @Override\n    public void onJsConfirm(WebView view, String url, String message, JsResult jsResult) {\n        super.onJsConfirm(view, url, message, jsResult);\n    }\n\n    @Override\n    public void onSelectItemsPrompt(WebView view, String url, String[] ways, Handler.Callback callback) {\n        showChooserInternal(view, url, ways, callback);\n    }\n\n    @Override\n    public void onForceDownloadAlert(String url, final Handler.Callback callback) {\n        super.onForceDownloadAlert(url, callback);\n    }\n\n    private void showChooserInternal(WebView view, String url, final String[] ways, final Handler.Callback callback) {\n        Activity mActivity;\n        if ((mActivity = this.mActivity) == null || mActivity.isFinishing()) {\n            return;\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {\n            if (mActivity.isDestroyed()) {\n                return;\n            }\n        }\n        LogUtils.i(TAG, \"url:\" + url + \"  ways:\" + ways[0]);\n        RecyclerView mRecyclerView;\n        if (mBottomSheetDialog == null) {\n            mBottomSheetDialog = new BottomSheetDialog(mActivity);\n            mRecyclerView = new RecyclerView(mActivity);\n            mRecyclerView.setLayoutManager(new LinearLayoutManager(mActivity));\n            mRecyclerView.setId(RECYCLERVIEW_ID);\n            mBottomSheetDialog.setContentView(mRecyclerView);\n        }\n        mRecyclerView = (RecyclerView) mBottomSheetDialog.getDelegate().findViewById(RECYCLERVIEW_ID);\n        mRecyclerView.setAdapter(getAdapter(ways, callback));\n        mBottomSheetDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {\n            @Override\n            public void onCancel(DialogInterface dialog) {\n                if (callback != null) {\n                    callback.handleMessage(Message.obtain(null, -1));\n                }\n            }\n        });\n        mBottomSheetDialog.show();\n    }\n\n    private RecyclerView.Adapter getAdapter(final String[] ways, final Handler.Callback callback) {\n        return new RecyclerView.Adapter<BottomSheetHolder>() {\n            @Override\n            public BottomSheetHolder onCreateViewHolder(ViewGroup viewGroup, int i) {\n                return new BottomSheetHolder(mLayoutInflater.inflate(android.R.layout.simple_list_item_1, viewGroup, false));\n            }\n\n            @Override\n            public void onBindViewHolder(BottomSheetHolder bottomSheetHolder, final int i) {\n                TypedValue outValue = new TypedValue();\n                mActivity.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);\n                bottomSheetHolder.mTextView.setBackgroundResource(outValue.resourceId);\n                bottomSheetHolder.mTextView.setText(ways[i]);\n                bottomSheetHolder.mTextView.setOnClickListener(new View.OnClickListener() {\n                    @Override\n                    public void onClick(View v) {\n\n                        if (mBottomSheetDialog != null && mBottomSheetDialog.isShowing()) {\n                            mBottomSheetDialog.dismiss();\n                        }\n                        Message mMessage = Message.obtain();\n                        mMessage.what = i;\n                        callback.handleMessage(mMessage);\n                    }\n                });\n            }\n\n            @Override\n            public int getItemCount() {\n                return ways.length;\n            }\n        };\n    }\n\n    private static class BottomSheetHolder extends RecyclerView.ViewHolder {\n        TextView mTextView;\n        public BottomSheetHolder(View itemView) {\n            super(itemView);\n            mTextView = (TextView) itemView.findViewById(android.R.id.text1);\n        }\n    }\n\n    @Override\n    public void onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult jsPromptResult) {\n        super.onJsPrompt(view, url, message, defaultValue, jsPromptResult);\n    }\n\n    @Override\n    protected void bindSupportWebParent(WebParentLayout webParentLayout, Activity activity) {\n        super.bindSupportWebParent(webParentLayout, activity);\n        this.mActivity = activity;\n        this.mWebParentLayout = webParentLayout;\n        mLayoutInflater = LayoutInflater.from(mActivity);\n    }\n\n    @Override\n    public void onShowMessage(String message, String from) {\n        Activity mActivity;\n        if ((mActivity = this.mActivity) == null || mActivity.isFinishing()) {\n            return;\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {\n            if (mActivity.isDestroyed()) {\n                return;\n            }\n        }\n        if (!TextUtils.isEmpty(from) && from.contains(\"performDownload\")) {\n            return;\n        }\n        onJsAlertInternal(mWebParentLayout.getWebView(), message);\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/DefaultDownloadImpl.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Message;\nimport android.webkit.WebView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.download.library.DownloadImpl;\nimport com.download.library.DownloadListenerAdapter;\nimport com.download.library.Extra;\nimport com.download.library.ResourceRequest;\n\nimport java.lang.ref.WeakReference;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * @author cenxiaozhong\n * @date 2017/5/13\n */\npublic class DefaultDownloadImpl implements android.webkit.DownloadListener {\n\t/**\n\t * Application Context\n\t */\n\tprotected Context mContext;\n\tprotected ConcurrentHashMap<String, ResourceRequest> mDownloadTasks = new ConcurrentHashMap<>();\n\t/**\n\t * Activity\n\t */\n\tprotected WeakReference<Activity> mActivityWeakReference = null;\n\t/**\n\t * TAG 用于打印，标识\n\t */\n\tprivate static final String TAG = DefaultDownloadImpl.class.getSimpleName();\n\t/**\n\t * 权限拦截\n\t */\n\tprotected PermissionInterceptor mPermissionListener = null;\n\t/**\n\t * AbsAgentWebUIController\n\t */\n\tprotected WeakReference<AbsAgentWebUIController> mAgentWebUIController;\n\n\tprivate static Handler mHandler = new Handler(Looper.getMainLooper());\n\n\n\tprotected DefaultDownloadImpl(Activity activity, WebView webView, PermissionInterceptor permissionInterceptor) {\n\t\tthis.mContext = activity.getApplicationContext();\n\t\tthis.mActivityWeakReference = new WeakReference<Activity>(activity);\n\t\tthis.mPermissionListener = permissionInterceptor;\n\t\tthis.mAgentWebUIController = new WeakReference<AbsAgentWebUIController>(AgentWebUtils.getAgentWebUIControllerByWebView(webView));\n\t}\n\n\n\t@Override\n\tpublic void onDownloadStart(final String url, final String userAgent, final String contentDisposition, final String mimetype, final long contentLength) {\n\t\tmHandler.post(new Runnable() {\n\t\t\t@Override\n\t\t\tpublic void run() {\n\t\t\t\tonDownloadStartInternal(url, userAgent, contentDisposition, mimetype, contentLength);\n\t\t\t}\n\t\t});\n\t}\n\n\tprotected void onDownloadStartInternal(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {\n\t\tif (null == mActivityWeakReference.get() || mActivityWeakReference.get().isFinishing()) {\n\t\t\treturn;\n\t\t}\n\t\tif (null != this.mPermissionListener) {\n\t\t\tif (this.mPermissionListener.intercept(url, AgentWebPermissions.STORAGE, \"download\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tResourceRequest resourceRequest = createResourceRequest(url);\n\t\tthis.mDownloadTasks.put(url, resourceRequest);\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n\t\t\tList<String> mList = null;\n\t\t\tif ((mList = checkNeedPermission()).isEmpty()) {\n\t\t\t\tpreDownload(url);\n\t\t\t} else {\n\t\t\t\tAction mAction = Action.createPermissionsAction(mList.toArray(new String[]{}));\n\t\t\t\tActionActivity.setPermissionListener(getPermissionListener(url));\n\t\t\t\tActionActivity.start(mActivityWeakReference.get(), mAction);\n\t\t\t}\n\t\t} else {\n\t\t\tpreDownload(url);\n\t\t}\n\t}\n\n\tprotected ResourceRequest createResourceRequest(String url) {\n\t\treturn DownloadImpl.getInstance().with(url).setEnableIndicator(true).autoOpenIgnoreMD5();\n\t}\n\n\tprotected ActionActivity.PermissionListener getPermissionListener(final String url) {\n\t\treturn new ActionActivity.PermissionListener() {\n\t\t\t@Override\n\t\t\tpublic void onRequestPermissionsResult(@NonNull String[] permissions, @NonNull int[] grantResults, Bundle extras) {\n\t\t\t\tif (checkNeedPermission().isEmpty()) {\n\t\t\t\t\tpreDownload(url);\n\t\t\t\t} else {\n\t\t\t\t\tif (null != mAgentWebUIController.get()) {\n\t\t\t\t\t\tmAgentWebUIController\n\t\t\t\t\t\t\t\t.get()\n\t\t\t\t\t\t\t\t.onPermissionsDeny(\n\t\t\t\t\t\t\t\t\t\tcheckNeedPermission().\n\t\t\t\t\t\t\t\t\t\t\t\ttoArray(new String[]{}),\n\t\t\t\t\t\t\t\t\t\tAgentWebPermissions.ACTION_STORAGE, \"Download\");\n\t\t\t\t\t}\n\t\t\t\t\tLogUtils.e(TAG, \"储存权限获取失败~\");\n\t\t\t\t}\n\n\t\t\t}\n\t\t};\n\t}\n\n\tprotected List<String> checkNeedPermission() {\n\t\tList<String> deniedPermissions = new ArrayList<>();\n\t\tif (!AgentWebUtils.hasPermission(mActivityWeakReference.get(), AgentWebPermissions.STORAGE)) {\n\t\t\tdeniedPermissions.addAll(Arrays.asList(AgentWebPermissions.STORAGE));\n\t\t}\n\t\treturn deniedPermissions;\n\t}\n\n\tprotected void preDownload(String url) {\n\t\t// 移动数据\n\t\tif (!isForceRequest(url) &&\n\t\t\t\tAgentWebUtils.checkNetworkType(mContext) > 1) {\n\t\t\tshowDialog(url);\n\t\t\treturn;\n\t\t}\n\t\tperformDownload(url);\n\t}\n\n\tprotected boolean isForceRequest(String url) {\n\t\tResourceRequest resourceRequest = mDownloadTasks.get(url);\n\t\tif (null != resourceRequest) {\n\t\t\treturn resourceRequest.getDownloadTask().isForceDownload();\n\t\t}\n\t\treturn false;\n\t}\n\n\tprotected void forceDownload(final String url) {\n\t\tResourceRequest resourceRequest = mDownloadTasks.get(url);\n\t\tresourceRequest.setForceDownload(true);\n\t\tperformDownload(url);\n\t}\n\n\tprotected void showDialog(final String url) {\n\t\tActivity mActivity;\n\t\tif (null == (mActivity = mActivityWeakReference.get()) || mActivity.isFinishing()) {\n\t\t\treturn;\n\t\t}\n\t\tAbsAgentWebUIController mAgentWebUIController;\n\t\tif (null != (mAgentWebUIController = this.mAgentWebUIController.get())) {\n\t\t\tmAgentWebUIController.onForceDownloadAlert(url, createCallback(url));\n\t\t}\n\t}\n\n\tprotected Handler.Callback createCallback(final String url) {\n\t\treturn new Handler.Callback() {\n\t\t\t@Override\n\t\t\tpublic boolean handleMessage(Message msg) {\n\t\t\t\tforceDownload(url);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t};\n\t}\n\n\tprotected void performDownload(String url) {\n\t\ttry {\n\t\t\tLogUtils.e(TAG, \"performDownload:\" + url + \" exist:\" + DownloadImpl.getInstance().exist(url));\n\t\t\t// 该链接是否正在下载\n\t\t\tif (DownloadImpl.getInstance().exist(url)) {\n\t\t\t\tif (null != mAgentWebUIController.get()) {\n\t\t\t\t\tmAgentWebUIController.get().onShowMessage(\n\t\t\t\t\t\t\tmActivityWeakReference.get()\n\t\t\t\t\t\t\t\t\t.getString(R.string.agentweb_download_task_has_been_exist), \"preDownload\");\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tResourceRequest resourceRequest = mDownloadTasks.get(url);\n\t\t\tresourceRequest.addHeader(\"Cookie\", AgentWebConfig.getCookiesByUrl(url));\n\t\t\ttaskEnqueue(resourceRequest);\n\t\t} catch (Throwable ignore) {\n\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\tignore.printStackTrace();\n\t\t\t}\n\t\t}\n\t}\n\n\tprotected void taskEnqueue(ResourceRequest resourceRequest) {\n\t\tresourceRequest.enqueue(new DownloadListenerAdapter() {\n\t\t\t@Override\n\t\t\tpublic boolean onResult(Throwable throwable, Uri path, String url, Extra extra) {\n\t\t\t\tmDownloadTasks.remove(url);\n\t\t\t\treturn super.onResult(throwable, path, url, extra);\n\t\t\t}\n\t\t});\n\t}\n\n\tpublic static DefaultDownloadImpl create(@NonNull Activity activity,\n\t                                         @NonNull WebView webView,\n\t                                         @Nullable PermissionInterceptor permissionInterceptor) {\n\t\ttry {\n\t\t\tDownloadImpl.getInstance().with(activity.getApplication());\n\t\t} catch (Throwable throwable) {\n\t\t\tLogUtils.e(TAG, \"implementation 'com.download.library:Downloader:x.x.x'\");\n\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\tthrowable.printStackTrace();\n\t\t\t}\n\t\t}\n\n\t\treturn new DefaultDownloadImpl(activity, webView, permissionInterceptor);\n\t}\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/DefaultUIController.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.app.Activity;\nimport android.app.ProgressDialog;\nimport android.content.DialogInterface;\nimport android.content.res.Resources;\nimport android.os.Build;\nimport android.os.Handler;\nimport android.os.Message;\nimport android.text.TextUtils;\nimport android.webkit.JsPromptResult;\nimport android.webkit.JsResult;\nimport android.webkit.WebView;\nimport android.widget.EditText;\n\nimport androidx.appcompat.app.AlertDialog;\n\n\n/**\n * @author cenxiaozhong\n * @date 2017/12/8\n * @since 3.0.0\n */\npublic class DefaultUIController extends AbsAgentWebUIController {\n\n\tprivate AlertDialog mAlertDialog;\n\tprotected AlertDialog mConfirmDialog;\n\tprivate JsPromptResult mJsPromptResult = null;\n\tprivate JsResult mJsResult = null;\n\tprivate AlertDialog mPromptDialog = null;\n\tprivate Activity mActivity;\n\tprivate WebParentLayout mWebParentLayout;\n\tprivate AlertDialog mAskOpenOtherAppDialog = null;\n\tprivate ProgressDialog mProgressDialog;\n\tprivate Resources mResources = null;\n\n\t@Override\n\tpublic void onJsAlert(WebView view, String url, String message) {\n\t\tAgentWebUtils.toastShowShort(view.getContext().getApplicationContext(), message);\n\t}\n\n\t@Override\n\tpublic void onOpenPagePrompt(WebView view, String url, final Handler.Callback callback) {\n\t\tLogUtils.i(TAG, \"onOpenPagePrompt\");\n\t\tActivity mActivity;\n\t\tif ((mActivity = this.mActivity) == null || mActivity.isFinishing()) {\n\t\t\treturn;\n\t\t}\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {\n\t\t\tif (mActivity.isDestroyed()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (mAskOpenOtherAppDialog == null) {\n\t\t\tmAskOpenOtherAppDialog = new AlertDialog\n\t\t\t\t\t.Builder(mActivity)\n\t\t\t\t\t.setMessage(mResources.getString(R.string.agentweb_leave_app_and_go_other_page,\n\t\t\t\t\t\t\tAgentWebUtils.getApplicationName(mActivity)))\n\t\t\t\t\t.setTitle(mResources.getString(R.string.agentweb_tips))\n\t\t\t\t\t.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic void onClick(DialogInterface dialog, int which) {\n\t\t\t\t\t\t\tif (callback != null) {\n\t\t\t\t\t\t\t\tcallback.handleMessage(Message.obtain(null, -1));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t})//\n\t\t\t\t\t.setPositiveButton(mResources.getString(R.string.agentweb_leave), new DialogInterface.OnClickListener() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic void onClick(DialogInterface dialog, int which) {\n\t\t\t\t\t\t\tif (callback != null) {\n\t\t\t\t\t\t\t\tcallback.handleMessage(Message.obtain(null, 1));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t\t.create();\n\t\t}\n\t\tmAskOpenOtherAppDialog.show();\n\t}\n\n\t@Override\n\tpublic void onJsConfirm(WebView view, String url, String message, JsResult jsResult) {\n\t\tonJsConfirmInternal(message, jsResult);\n\t}\n\n\t@Override\n\tpublic void onSelectItemsPrompt(WebView view, String url, final String[] ways, final Handler.Callback callback) {\n\t\tshowChooserInternal(ways, callback);\n\t}\n\n\t@Override\n\tpublic void onForceDownloadAlert(String url, final Handler.Callback callback) {\n\t\tonForceDownloadAlertInternal(callback);\n\t}\n\n\tprivate void onForceDownloadAlertInternal(final Handler.Callback callback) {\n\t\tActivity mActivity;\n\t\tif ((mActivity = this.mActivity) == null || mActivity.isFinishing()) {\n\t\t\treturn;\n\t\t}\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {\n\t\t\tif (mActivity.isDestroyed()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tAlertDialog mAlertDialog = null;\n\t\tmAlertDialog = new AlertDialog.Builder(mActivity)\n\t\t\t\t.setTitle(mResources.getString(R.string.agentweb_tips))\n\t\t\t\t.setMessage(mResources.getString(R.string.agentweb_honeycomblow))\n\t\t\t\t.setNegativeButton(mResources.getString(R.string.agentweb_download), new DialogInterface.OnClickListener() {\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic void onClick(DialogInterface dialog, int which) {\n\t\t\t\t\t\tif (dialog != null) {\n\t\t\t\t\t\t\tdialog.dismiss();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (callback != null) {\n\t\t\t\t\t\t\tcallback.handleMessage(Message.obtain());\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t})//\n\t\t\t\t.setPositiveButton(mResources.getString(R.string.agentweb_cancel), new DialogInterface.OnClickListener() {\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic void onClick(DialogInterface dialog, int which) {\n\n\t\t\t\t\t\tif (dialog != null) {\n\t\t\t\t\t\t\tdialog.dismiss();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}).create();\n\t\tmAlertDialog.show();\n\t}\n\n\tprivate void showChooserInternal(String[] ways, final Handler.Callback callback) {\n\t\tActivity mActivity;\n\t\tif ((mActivity = this.mActivity) == null || mActivity.isFinishing()) {\n\t\t\treturn;\n\t\t}\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {\n\t\t\tif (mActivity.isDestroyed()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tmAlertDialog = new AlertDialog.Builder(mActivity)\n\t\t\t\t.setSingleChoiceItems(ways, -1, new DialogInterface.OnClickListener() {\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic void onClick(DialogInterface dialog, int which) {\n\t\t\t\t\t\tdialog.dismiss();\n\t\t\t\t\t\tLogUtils.i(TAG, \"which:\" + which);\n\t\t\t\t\t\tif (callback != null) {\n\t\t\t\t\t\t\tMessage mMessage = Message.obtain();\n\t\t\t\t\t\t\tmMessage.what = which;\n\t\t\t\t\t\t\tcallback.handleMessage(mMessage);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\t\t\t\t}).setOnCancelListener(new DialogInterface.OnCancelListener() {\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic void onCancel(DialogInterface dialog) {\n\t\t\t\t\t\tdialog.dismiss();\n\t\t\t\t\t\tif (callback != null) {\n\t\t\t\t\t\t\tcallback.handleMessage(Message.obtain(null, -1));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}).create();\n\t\tmAlertDialog.show();\n\t}\n\n\tprivate void onJsConfirmInternal(String message, JsResult jsResult) {\n\t\tLogUtils.i(TAG, \"activity:\" + mActivity.hashCode() + \"  \");\n\t\tActivity mActivity = this.mActivity;\n\t\tif (mActivity == null || mActivity.isFinishing()) {\n\t\t\ttoCancelJsresult(jsResult);\n\t\t\treturn;\n\t\t}\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {\n\t\t\tif (mActivity.isDestroyed()) {\n\t\t\t\ttoCancelJsresult(jsResult);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tif (mConfirmDialog == null) {\n\t\t\tmConfirmDialog = new AlertDialog.Builder(mActivity)\n\t\t\t\t\t.setMessage(message)\n\t\t\t\t\t.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic void onClick(DialogInterface dialog, int which) {\n\t\t\t\t\t\t\ttoDismissDialog(mConfirmDialog);\n\t\t\t\t\t\t\ttoCancelJsresult(mJsResult);\n\t\t\t\t\t\t}\n\t\t\t\t\t})//\n\t\t\t\t\t.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic void onClick(DialogInterface dialog, int which) {\n\t\t\t\t\t\t\ttoDismissDialog(mConfirmDialog);\n\t\t\t\t\t\t\tif (mJsResult != null) {\n\t\t\t\t\t\t\t\tmJsResult.confirm();\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t\t.setOnCancelListener(new DialogInterface.OnCancelListener() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic void onCancel(DialogInterface dialog) {\n\t\t\t\t\t\t\tdialog.dismiss();\n\t\t\t\t\t\t\ttoCancelJsresult(mJsResult);\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t\t.create();\n\n\t\t}\n\t\tmConfirmDialog.setMessage(message);\n\t\tthis.mJsResult = jsResult;\n\t\tmConfirmDialog.show();\n\t}\n\n\n\tprivate void onJsPromptInternal(String message, String defaultValue, JsPromptResult jsPromptResult) {\n\t\tActivity mActivity = this.mActivity;\n\t\tif (mActivity == null || mActivity.isFinishing()) {\n\t\t\tjsPromptResult.cancel();\n\t\t\treturn;\n\t\t}\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {\n\t\t\tif (mActivity.isDestroyed()) {\n\t\t\t\tjsPromptResult.cancel();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (mPromptDialog == null) {\n\t\t\tfinal EditText et = new EditText(mActivity);\n\t\t\tet.setText(defaultValue);\n\t\t\tmPromptDialog = new AlertDialog.Builder(mActivity)\n\t\t\t\t\t.setView(et)\n\t\t\t\t\t.setTitle(message)\n\t\t\t\t\t.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic void onClick(DialogInterface dialog, int which) {\n\t\t\t\t\t\t\ttoDismissDialog(mPromptDialog);\n\t\t\t\t\t\t\ttoCancelJsresult(mJsPromptResult);\n\t\t\t\t\t\t}\n\t\t\t\t\t})//\n\t\t\t\t\t.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic void onClick(DialogInterface dialog, int which) {\n\t\t\t\t\t\t\ttoDismissDialog(mPromptDialog);\n\n\t\t\t\t\t\t\tif (mJsPromptResult != null) {\n\t\t\t\t\t\t\t\tmJsPromptResult.confirm(et.getText().toString());\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t\t.setOnCancelListener(new DialogInterface.OnCancelListener() {\n\t\t\t\t\t\t@Override\n\t\t\t\t\t\tpublic void onCancel(DialogInterface dialog) {\n\t\t\t\t\t\t\tdialog.dismiss();\n\t\t\t\t\t\t\ttoCancelJsresult(mJsPromptResult);\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t\t.create();\n\t\t}\n\t\tthis.mJsPromptResult = jsPromptResult;\n\t\tmPromptDialog.show();\n\t}\n\n\t@Override\n\tpublic void onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult jsPromptResult) {\n\t\tonJsPromptInternal(message, defaultValue, jsPromptResult);\n\t}\n\n\t@Override\n\tpublic void onMainFrameError(WebView view, int errorCode, String description, String failingUrl) {\n\n\t\tLogUtils.i(TAG, \"mWebParentLayout onMainFrameError:\" + mWebParentLayout);\n\t\tif (mWebParentLayout != null) {\n\t\t\tmWebParentLayout.showPageMainFrameError();\n\t\t}\n\t}\n\n\t@Override\n\tpublic void onShowMainFrame() {\n\t\tif (mWebParentLayout != null) {\n\t\t\tmWebParentLayout.hideErrorLayout();\n\t\t}\n\t}\n\n\t@Override\n\tpublic void onLoading(String msg) {\n\t\tActivity mActivity;\n\t\tif ((mActivity = this.mActivity) == null || mActivity.isFinishing()) {\n\t\t\treturn;\n\t\t}\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {\n\t\t\tif (mActivity.isDestroyed()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (mProgressDialog == null) {\n\t\t\tmProgressDialog = new ProgressDialog(mActivity);\n\t\t}\n\t\tmProgressDialog.setCancelable(false);\n\t\tmProgressDialog.setCanceledOnTouchOutside(false);\n\t\tmProgressDialog.setMessage(msg);\n\t\tmProgressDialog.show();\n\n\t}\n\n\t@Override\n\tpublic void onCancelLoading() {\n\t\tActivity mActivity;\n\t\tif ((mActivity = this.mActivity) == null || mActivity.isFinishing()) {\n\t\t\treturn;\n\t\t}\n\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {\n\t\t\tif (mActivity.isDestroyed()) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (mProgressDialog != null && mProgressDialog.isShowing()) {\n\t\t\tmProgressDialog.dismiss();\n\t\t}\n\t\tmProgressDialog = null;\n\t}\n\n\t@Override\n\tpublic void onShowMessage(String message, String from) {\n\t\tif (!TextUtils.isEmpty(from) && from.contains(\"performDownload\")) {\n\t\t\treturn;\n\t\t}\n\t\tAgentWebUtils.toastShowShort(mActivity.getApplicationContext(), message);\n\t}\n\n\t@Override\n\tpublic void onPermissionsDeny(String[] permissions, String permissionType, String action) {\n//\t\tAgentWebUtils.toastShowShort(mActivity.getApplicationContext(), \"权限被冻结\");\n\t}\n\n\tprivate void toCancelJsresult(JsResult result) {\n\t\tif (result != null) {\n\t\t\tresult.cancel();\n\t\t}\n\t}\n\n\n\t@Override\n\tprotected void bindSupportWebParent(WebParentLayout webParentLayout, Activity activity) {\n\t\tthis.mActivity = activity;\n\t\tthis.mWebParentLayout = webParentLayout;\n\t\tmResources = this.mActivity.getResources();\n\n\t}\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/DefaultWebClient.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.annotation.TargetApi;\nimport android.app.Activity;\nimport android.content.ActivityNotFoundException;\nimport android.content.Intent;\nimport android.content.pm.ActivityInfo;\nimport android.content.pm.PackageManager;\nimport android.content.pm.ResolveInfo;\nimport android.graphics.Bitmap;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Handler;\nimport android.os.Message;\nimport android.text.TextUtils;\nimport android.view.KeyEvent;\nimport android.view.View;\nimport android.webkit.HttpAuthHandler;\nimport android.webkit.WebResourceError;\nimport android.webkit.WebResourceRequest;\nimport android.webkit.WebResourceResponse;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\n\nimport androidx.annotation.RequiresApi;\n\nimport com.alipay.sdk.app.H5PayCallback;\nimport com.alipay.sdk.app.PayTask;\nimport com.alipay.sdk.util.H5PayResultModel;\n\nimport java.lang.ref.WeakReference;\nimport java.lang.reflect.Constructor;\nimport java.lang.reflect.Method;\nimport java.net.URISyntaxException;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * @author cenxiaozhong\n * @since 3.0.0\n */\npublic class DefaultWebClient extends MiddlewareWebClientBase {\n\t/**\n\t * Activity's WeakReference\n\t */\n\tprivate WeakReference<Activity> mWeakReference = null;\n\t/**\n\t * 缩放\n\t */\n\tprivate static final int CONSTANTS_ABNORMAL_BIG = 7;\n\t/**\n\t * WebViewClient\n\t */\n\tprivate WebViewClient mWebViewClient;\n\t/**\n\t * mWebClientHelper\n\t */\n\tprivate boolean webClientHelper = true;\n\t/**\n\t * intent ' s scheme\n\t */\n\tpublic static final String INTENT_SCHEME = \"intent://\";\n\t/**\n\t * Wechat pay scheme ，用于唤醒微信支付\n\t */\n\tpublic static final String WEBCHAT_PAY_SCHEME = \"weixin://wap/pay?\";\n\t/**\n\t * 支付宝\n\t */\n\tpublic static final String ALIPAYS_SCHEME = \"alipays://\";\n\t/**\n\t * http scheme\n\t */\n\tpublic static final String HTTP_SCHEME = \"http://\";\n\t/**\n\t * https scheme\n\t */\n\tpublic static final String HTTPS_SCHEME = \"https://\";\n\t/**\n\t * true 表示当前应用内依赖了 alipay library , false  反之\n\t */\n\tprivate static final boolean HAS_ALIPAY_LIB;\n\t/**\n\t * WebViewClient's tag 用于打印\n\t */\n\tprivate static final String TAG = DefaultWebClient.class.getSimpleName();\n\t/**\n\t * 直接打开其他页面\n\t */\n\tpublic static final int DERECT_OPEN_OTHER_PAGE = 1001;\n\t/**\n\t * 弹窗咨询用户是否前往其他页面\n\t */\n\tpublic static final int ASK_USER_OPEN_OTHER_PAGE = DERECT_OPEN_OTHER_PAGE >> 2;\n\t/**\n\t * 不允许打开其他页面\n\t */\n\tpublic static final int DISALLOW_OPEN_OTHER_APP = DERECT_OPEN_OTHER_PAGE >> 4;\n\t/**\n\t * 默认为咨询用户\n\t */\n\tprivate int mUrlHandleWays = ASK_USER_OPEN_OTHER_PAGE;\n\t/**\n\t * 是否拦截找不到相应页面的Url，默认拦截\n\t */\n\tprivate boolean mIsInterceptUnkownUrl = true;\n\t/**\n\t * AbsAgentWebUIController\n\t */\n\tprivate WeakReference<AbsAgentWebUIController> mAgentWebUIController = null;\n\t/**\n\t * WebView\n\t */\n\tprivate WebView mWebView;\n\t/**\n\t * 弹窗回调\n\t */\n\tprivate Handler.Callback mCallback = null;\n\t/**\n\t * MainFrameErrorMethod\n\t */\n\tprivate Method onMainFrameErrorMethod = null;\n\t/**\n\t * Alipay PayTask 对象\n\t */\n\tprivate Object mPayTask;\n\t/**\n\t * SMS scheme\n\t */\n\tpublic static final String SCHEME_SMS = \"sms:\";\n\t/**\n\t * 缓存当前出现错误的页面\n\t */\n\tprivate Set<String> mErrorUrlsSet = new HashSet<>();\n\t/**\n\t * 缓存等待加载完成的页面 onPageStart()执行之后 ，onPageFinished()执行之前\n\t */\n\tprivate Set<String> mWaittingFinishSet = new HashSet<>();\n\n\tstatic {\n\t\tboolean tag = true;\n\t\ttry {\n\t\t\tClass.forName(\"com.alipay.sdk.app.PayTask\");\n\t\t} catch (Throwable ignore) {\n\t\t\ttag = false;\n\t\t}\n\t\tHAS_ALIPAY_LIB = tag;\n\t\tLogUtils.i(TAG, \"HAS_ALIPAY_LIB:\" + HAS_ALIPAY_LIB);\n\t}\n\n\n\tDefaultWebClient(Builder builder) {\n\t\tsuper(builder.mClient);\n\t\tthis.mWebView = builder.mWebView;\n\t\tthis.mWebViewClient = builder.mClient;\n\t\tmWeakReference = new WeakReference<Activity>(builder.mActivity);\n\t\tthis.webClientHelper = builder.mWebClientHelper;\n\t\tmAgentWebUIController = new WeakReference<AbsAgentWebUIController>(AgentWebUtils.getAgentWebUIControllerByWebView(builder.mWebView));\n\t\tmIsInterceptUnkownUrl = builder.mIsInterceptUnkownScheme;\n\t\tif (builder.mUrlHandleWays <= 0) {\n\t\t\tmUrlHandleWays = ASK_USER_OPEN_OTHER_PAGE;\n\t\t} else {\n\t\t\tmUrlHandleWays = builder.mUrlHandleWays;\n\t\t}\n\t}\n\n\t@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n\t@Override\n\tpublic boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {\n\t\tString url = request.getUrl().toString();\n\t\tif (url.startsWith(HTTP_SCHEME) || url.startsWith(HTTPS_SCHEME)) {\n\t\t\treturn (webClientHelper && HAS_ALIPAY_LIB && isAlipay(view, url));\n\t\t}\n\t\tif (!webClientHelper) {\n\t\t\treturn super.shouldOverrideUrlLoading(view, request);\n\t\t}\n\t\tif (handleCommonLink(url)) {\n\t\t\treturn true;\n\t\t}\n\t\t// intent\n\t\tif (url.startsWith(INTENT_SCHEME)) {\n\t\t\thandleIntentUrl(url);\n\t\t\tLogUtils.i(TAG, \"intent url \");\n\t\t\treturn true;\n\t\t}\n\t\t// 微信支付\n\t\tif (url.startsWith(WEBCHAT_PAY_SCHEME)) {\n\t\t\tLogUtils.i(TAG, \"lookup wechat to pay ~~\");\n\t\t\tstartActivity(url);\n\t\t\treturn true;\n\t\t}\n\t\tif (url.startsWith(ALIPAYS_SCHEME) && lookup(url)) {\n\t\t\tLogUtils.i(TAG, \"alipays url lookup alipay ~~ \");\n\t\t\treturn true;\n\t\t}\n\t\tif (queryActiviesNumber(url) > 0 && deepLink(url)) {\n\t\t\tLogUtils.i(TAG, \"intercept url:\" + url);\n\t\t\treturn true;\n\t\t}\n\t\tif (mIsInterceptUnkownUrl) {\n\t\t\tLogUtils.i(TAG, \"intercept UnkownUrl :\" + request.getUrl());\n\t\t\treturn true;\n\t\t}\n\t\treturn super.shouldOverrideUrlLoading(view, request);\n\t}\n\n\t@Override\n\tpublic WebResourceResponse shouldInterceptRequest(WebView view, String url) {\n\t\treturn super.shouldInterceptRequest(view, url);\n\t}\n\n\t@Override\n\tpublic void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {\n\t\tsuper.onReceivedHttpAuthRequest(view, handler, host, realm);\n\t}\n\n\tprivate boolean deepLink(String url) {\n\t\tswitch (mUrlHandleWays) {\n\t\t\t// 直接打开其他App\n\t\t\tcase DERECT_OPEN_OTHER_PAGE:\n\t\t\t\tlookup(url);\n\t\t\t\treturn true;\n\t\t\t// 咨询用户是否打开其他App\n\t\t\tcase ASK_USER_OPEN_OTHER_PAGE:\n\t\t\t\tActivity mActivity = null;\n\t\t\t\tif ((mActivity = mWeakReference.get()) == null) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tResolveInfo resolveInfo = lookupResolveInfo(url);\n\t\t\t\tif (null == resolveInfo) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tActivityInfo activityInfo = resolveInfo.activityInfo;\n\t\t\t\tLogUtils.e(TAG, \"resolve package:\" + resolveInfo.activityInfo.packageName + \" app package:\" + mActivity.getPackageName());\n\t\t\t\tif (activityInfo != null\n\t\t\t\t\t\t&& !TextUtils.isEmpty(activityInfo.packageName)\n\t\t\t\t\t\t&& activityInfo.packageName.equals(mActivity.getPackageName())) {\n\t\t\t\t\treturn lookup(url);\n\t\t\t\t}\n\t\t\t\tif (mAgentWebUIController.get() != null) {\n\t\t\t\t\tmAgentWebUIController.get()\n\t\t\t\t\t\t\t.onOpenPagePrompt(this.mWebView,\n\t\t\t\t\t\t\t\t\tmWebView.getUrl(),\n\t\t\t\t\t\t\t\t\tgetCallback(url));\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t// 默认不打开\n\t\t\tdefault:\n\t\t\t\treturn false;\n\t\t}\n\t}\n\n\t@Override\n\tpublic WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {\n\t\treturn super.shouldInterceptRequest(view, request);\n\t}\n\n\t@Override\n\tpublic boolean shouldOverrideUrlLoading(WebView view, String url) {\n\t\tif (url.startsWith(HTTP_SCHEME) || url.startsWith(HTTPS_SCHEME)) {\n\t\t\treturn (webClientHelper && HAS_ALIPAY_LIB && isAlipay(view, url));\n\t\t}\n\t\tif (!webClientHelper) {\n\t\t\treturn false;\n\t\t}\n\t\t//电话 ， 邮箱 ， 短信\n\t\tif (handleCommonLink(url)) {\n\t\t\treturn true;\n\t\t}\n\t\t//Intent scheme\n\t\tif (url.startsWith(INTENT_SCHEME)) {\n\t\t\thandleIntentUrl(url);\n\t\t\treturn true;\n\t\t}\n\t\t//微信支付\n\t\tif (url.startsWith(WEBCHAT_PAY_SCHEME)) {\n\t\t\tstartActivity(url);\n\t\t\treturn true;\n\t\t}\n\t\t//支付宝\n\t\tif (url.startsWith(ALIPAYS_SCHEME) && lookup(url)) {\n\t\t\treturn true;\n\t\t}\n\t\t//打开url 相对应的页面\n\t\tif (queryActiviesNumber(url) > 0 && deepLink(url)) {\n\t\t\tLogUtils.i(TAG, \"intercept OtherAppScheme\");\n\t\t\treturn true;\n\t\t}\n\t\t// 手机里面没有页面能匹配到该链接 ，拦截下来。\n\t\tif (mIsInterceptUnkownUrl) {\n\t\t\tLogUtils.i(TAG, \"intercept InterceptUnkownScheme : \" + url);\n\t\t\treturn true;\n\t\t}\n\t\treturn super.shouldOverrideUrlLoading(view, url);\n\t}\n\n\n\tprivate int queryActiviesNumber(String url) {\n\t\ttry {\n\t\t\tif (mWeakReference.get() == null) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\tIntent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);\n\t\t\tPackageManager mPackageManager = mWeakReference.get().getPackageManager();\n\t\t\tList<ResolveInfo> mResolveInfos = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);\n\t\t\treturn mResolveInfos == null ? 0 : mResolveInfos.size();\n\t\t} catch (URISyntaxException ignore) {\n\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\tignore.printStackTrace();\n\t\t\t}\n\t\t\treturn 0;\n\t\t}\n\t}\n\n\tprivate void handleIntentUrl(String intentUrl) {\n\t\ttry {\n\t\t\tIntent intent = null;\n\t\t\tif (TextUtils.isEmpty(intentUrl) || !intentUrl.startsWith(INTENT_SCHEME)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (lookup(intentUrl)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch (Throwable e) {\n\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\te.printStackTrace();\n\t\t\t}\n\t\t}\n\t}\n\n\n\tprivate ResolveInfo lookupResolveInfo(String url) {\n\t\ttry {\n\t\t\tIntent intent;\n\t\t\tActivity mActivity = null;\n\t\t\tif ((mActivity = mWeakReference.get()) == null) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tPackageManager packageManager = mActivity.getPackageManager();\n\t\t\tintent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);\n\t\t\tResolveInfo info = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);\n\t\t\treturn info;\n\t\t} catch (Throwable ignore) {\n\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\tignore.printStackTrace();\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\tprivate boolean lookup(String url) {\n\t\ttry {\n\t\t\tIntent intent;\n\t\t\tActivity mActivity = null;\n\t\t\tif ((mActivity = mWeakReference.get()) == null) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tPackageManager packageManager = mActivity.getPackageManager();\n\t\t\tintent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);\n\t\t\tResolveInfo info = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);\n\t\t\t// 跳到该应用\n\t\t\tif (info != null) {\n\t\t\t\tmActivity.startActivity(intent);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} catch (Throwable ignore) {\n\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\tignore.printStackTrace();\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate boolean isAlipay(final WebView view, String url) {\n\t\ttry {\n\t\t\tActivity mActivity = null;\n\t\t\tif ((mActivity = mWeakReference.get()) == null) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t/**\n\t\t\t * 推荐采用的新的二合一接口(payInterceptorWithUrl),只需调用一次\n\t\t\t */\n\t\t\tif (mPayTask == null) {\n\t\t\t\tClass clazz = Class.forName(\"com.alipay.sdk.app.PayTask\");\n\t\t\t\tConstructor<?> mConstructor = clazz.getConstructor(Activity.class);\n\t\t\t\tmPayTask = mConstructor.newInstance(mActivity);\n\t\t\t}\n\t\t\tfinal PayTask task = (PayTask) mPayTask;\n\t\t\tboolean isIntercepted = task.payInterceptorWithUrl(url, true, new H5PayCallback() {\n\t\t\t\t@Override\n\t\t\t\tpublic void onPayResult(final H5PayResultModel result) {\n\t\t\t\t\tfinal String url = result.getReturnUrl();\n\t\t\t\t\tif (!TextUtils.isEmpty(url)) {\n\t\t\t\t\t\tAgentWebUtils.runInUiThread(new Runnable() {\n\t\t\t\t\t\t\t@Override\n\t\t\t\t\t\t\tpublic void run() {\n\t\t\t\t\t\t\t\tview.loadUrl(url);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t\tif (isIntercepted) {\n\t\t\t\tLogUtils.i(TAG, \"alipay-isIntercepted:\" + isIntercepted + \"  url:\" + url);\n\t\t\t}\n\t\t\treturn isIntercepted;\n\t\t} catch (Throwable ignore) {\n\t\t\tif (AgentWebConfig.DEBUG) {\n\t\t\t\tignore.printStackTrace();\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\n\tprivate boolean handleCommonLink(String url) {\n\t\tif (url.startsWith(WebView.SCHEME_TEL)\n\t\t\t\t|| url.startsWith(SCHEME_SMS)\n\t\t\t\t|| url.startsWith(WebView.SCHEME_MAILTO)\n\t\t\t\t|| url.startsWith(WebView.SCHEME_GEO)) {\n\t\t\ttry {\n\t\t\t\tActivity mActivity = null;\n\t\t\t\tif ((mActivity = mWeakReference.get()) == null) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tIntent intent = new Intent(Intent.ACTION_VIEW);\n\t\t\t\tintent.setData(Uri.parse(url));\n\t\t\t\tmActivity.startActivity(intent);\n\t\t\t} catch (ActivityNotFoundException ignored) {\n\t\t\t\tif (AgentWebConfig.DEBUG) {\n\t\t\t\t\tignored.printStackTrace();\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic void onPageStarted(WebView view, String url, Bitmap favicon) {\n\t\tif (!mWaittingFinishSet.contains(url)) {\n\t\t\tmWaittingFinishSet.add(url);\n\t\t}\n\t\tsuper.onPageStarted(view, url, favicon);\n\n\t}\n\n\n\t/**\n\t * MainFrame Error\n\t *\n\t * @param view\n\t * @param errorCode\n\t * @param description\n\t * @param failingUrl\n\t */\n\t@Override\n\tpublic void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {\n\t\tLogUtils.i(TAG, \"onReceivedError：\" + description + \"  CODE:\" + errorCode);\n\t\tonMainFrameError(view, errorCode, description, failingUrl);\n\t}\n\n\n\t@TargetApi(Build.VERSION_CODES.M)\n\t@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n\t@Override\n\tpublic void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {\n\t\tif (request.isForMainFrame()) {\n\t\t\tonMainFrameError(view,\n\t\t\t\t\terror.getErrorCode(), error.getDescription().toString(),\n\t\t\t\t\trequest.getUrl().toString());\n\t\t}\n\t\tLogUtils.i(TAG, \"onReceivedError:\" + error.getDescription() + \" code:\" + error.getErrorCode());\n\t}\n\n\tprivate void onMainFrameError(WebView view, int errorCode, String description, String failingUrl) {\n\t\tmErrorUrlsSet.add(failingUrl);\n\t\t// 下面逻辑判断开发者是否重写了 onMainFrameError 方法 ， 优先交给开发者处理\n\t\tif (this.mWebViewClient != null && webClientHelper) {\n\t\t\tMethod mMethod = this.onMainFrameErrorMethod;\n\t\t\tif (mMethod != null || (this.onMainFrameErrorMethod = mMethod = AgentWebUtils.isExistMethod(mWebViewClient, \"onMainFrameError\", AbsAgentWebUIController.class, WebView.class, int.class, String.class, String.class)) != null) {\n\t\t\t\ttry {\n\t\t\t\t\tmMethod.invoke(this.mWebViewClient, mAgentWebUIController.get(), view, errorCode, description, failingUrl);\n\t\t\t\t} catch (Throwable ignore) {\n\t\t\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\t\t\tignore.printStackTrace();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (mAgentWebUIController.get() != null) {\n\t\t\tmAgentWebUIController.get().onMainFrameError(view, errorCode, description, failingUrl);\n\t\t}\n//        this.mWebView.setVisibility(View.GONE);\n\t}\n\n\n\t@Override\n\tpublic void onPageFinished(WebView view, String url) {\n\t\tif (!mErrorUrlsSet.contains(url) && mWaittingFinishSet.contains(url)) {\n\t\t\tif (mAgentWebUIController.get() != null) {\n\t\t\t\tmAgentWebUIController.get().onShowMainFrame();\n\t\t\t}\n\t\t} else {\n\t\t\tview.setVisibility(View.VISIBLE);\n\t\t}\n\t\tif (mWaittingFinishSet.contains(url)) {\n\t\t\tmWaittingFinishSet.remove(url);\n\t\t}\n\t\tif (!mErrorUrlsSet.isEmpty()) {\n\t\t\tmErrorUrlsSet.clear();\n\t\t}\n\t\tsuper.onPageFinished(view, url);\n\t}\n\n\n\t@Override\n\tpublic boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {\n\t\treturn super.shouldOverrideKeyEvent(view, event);\n\t}\n\n\n\tprivate void startActivity(String url) {\n\t\ttry {\n\t\t\tif (mWeakReference.get() == null) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tIntent intent = new Intent();\n\t\t\tintent.setAction(Intent.ACTION_VIEW);\n\t\t\tintent.setData(Uri.parse(url));\n\t\t\tmWeakReference.get().startActivity(intent);\n\n\t\t} catch (Exception e) {\n\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\te.printStackTrace();\n\t\t\t}\n\t\t}\n\t}\n\n\t@Override\n\tpublic void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {\n\t\tsuper.onReceivedHttpError(view, request, errorResponse);\n\t}\n\n\t@Override\n\tpublic void onScaleChanged(WebView view, float oldScale, float newScale) {\n\t\tLogUtils.i(TAG, \"onScaleChanged:\" + oldScale + \"   n:\" + newScale);\n\t\tif (newScale - oldScale > CONSTANTS_ABNORMAL_BIG) {\n\t\t\tview.setInitialScale((int) (oldScale / newScale * 100));\n\t\t}\n\t}\n\n\tprivate Handler.Callback getCallback(final String url) {\n\t\tif (this.mCallback != null) {\n\t\t\treturn this.mCallback;\n\t\t}\n\t\treturn this.mCallback = new Handler.Callback() {\n\t\t\t@Override\n\t\t\tpublic boolean handleMessage(Message msg) {\n\t\t\t\tswitch (msg.what) {\n\t\t\t\t\tcase 1:\n\t\t\t\t\t\tlookup(url);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\t\t};\n\t}\n\n\n\tpublic static Builder createBuilder() {\n\t\treturn new Builder();\n\t}\n\n\tpublic static class Builder {\n\t\tprivate Activity mActivity;\n\t\tprivate WebViewClient mClient;\n\t\tprivate boolean mWebClientHelper;\n\t\tprivate PermissionInterceptor mPermissionInterceptor;\n\t\tprivate WebView mWebView;\n\t\tprivate boolean mIsInterceptUnkownScheme;\n\t\tprivate int mUrlHandleWays;\n\n\t\tpublic Builder setActivity(Activity activity) {\n\t\t\tthis.mActivity = activity;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic Builder setClient(WebViewClient client) {\n\t\t\tthis.mClient = client;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic Builder setWebClientHelper(boolean webClientHelper) {\n\t\t\tthis.mWebClientHelper = webClientHelper;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic Builder setPermissionInterceptor(PermissionInterceptor permissionInterceptor) {\n\t\t\tthis.mPermissionInterceptor = permissionInterceptor;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic Builder setWebView(WebView webView) {\n\t\t\tthis.mWebView = webView;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic Builder setInterceptUnkownUrl(boolean interceptUnkownScheme) {\n\t\t\tthis.mIsInterceptUnkownScheme = interceptUnkownScheme;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic Builder setUrlHandleWays(int urlHandleWays) {\n\t\t\tthis.mUrlHandleWays = urlHandleWays;\n\t\t\treturn this;\n\t\t}\n\n\t\tpublic DefaultWebClient build() {\n\t\t\treturn new DefaultWebClient(this);\n\t\t}\n\t}\n\n\tpublic static enum OpenOtherPageWays {\n\t\t/**\n\t\t * 直接打开跳转页\n\t\t */\n\t\tDERECT(DefaultWebClient.DERECT_OPEN_OTHER_PAGE),\n\t\t/**\n\t\t * 咨询用户是否打开\n\t\t */\n\t\tASK(DefaultWebClient.ASK_USER_OPEN_OTHER_PAGE),\n\t\t/**\n\t\t * 禁止打开其他页面\n\t\t */\n\t\tDISALLOW(DefaultWebClient.DISALLOW_OPEN_OTHER_APP);\n\t\tint code;\n\n\t\tOpenOtherPageWays(int code) {\n\t\t\tthis.code = code;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/DefaultWebCreator.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.app.Activity;\nimport android.graphics.Color;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.ViewStub;\nimport android.webkit.WebView;\nimport android.widget.FrameLayout;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\npublic class DefaultWebCreator implements WebCreator {\n    private Activity mActivity;\n    private ViewGroup mViewGroup;\n    private boolean mIsNeedDefaultProgress;\n    private int mIndex;\n    private BaseIndicatorView mProgressView;\n    private ViewGroup.LayoutParams mLayoutParams = null;\n    private int mColor = -1;\n    /**\n     * 单位dp\n     */\n    private int mHeight;\n    private boolean mIsCreated = false;\n    private IWebLayout mIWebLayout;\n    private BaseIndicatorSpec mBaseIndicatorSpec;\n    private WebView mWebView = null;\n    private FrameLayout mFrameLayout = null;\n    private View mTargetProgress;\n    private static final String TAG = DefaultWebCreator.class.getSimpleName();\n\n\t/**\n\t * 使用默认的进度条\n\t * @param activity\n\t * @param viewGroup\n\t * @param lp\n\t * @param index\n\t * @param color\n\t * @param mHeight\n\t * @param webView\n\t * @param webLayout\n\t */\n    protected DefaultWebCreator(@NonNull Activity activity,\n                                @Nullable ViewGroup viewGroup,\n                                ViewGroup.LayoutParams lp,\n                                int index,\n                                int color,\n                                int mHeight,\n                                WebView webView,\n                                IWebLayout webLayout) {\n        this.mActivity = activity;\n        this.mViewGroup = viewGroup;\n        this.mIsNeedDefaultProgress = true;\n        this.mIndex = index;\n        this.mColor = color;\n        this.mLayoutParams = lp;\n        this.mHeight = mHeight;\n        this.mWebView = webView;\n        this.mIWebLayout = webLayout;\n    }\n\n\t/**\n\t * 关闭进度条\n\t * @param activity\n\t * @param viewGroup\n\t * @param lp\n\t * @param index\n\t * @param webView\n\t * @param webLayout\n\t */\n    protected DefaultWebCreator(@NonNull Activity activity, @Nullable ViewGroup viewGroup, ViewGroup.LayoutParams lp, int index, @Nullable WebView webView, IWebLayout webLayout) {\n        this.mActivity = activity;\n        this.mViewGroup = viewGroup;\n        this.mIsNeedDefaultProgress = false;\n        this.mIndex = index;\n        this.mLayoutParams = lp;\n        this.mWebView = webView;\n        this.mIWebLayout = webLayout;\n    }\n\n    /**\n     * 自定义Indicator\n     * @param activity\n     * @param viewGroup\n     * @param lp\n     * @param index\n     * @param progressView\n     * @param webView\n     * @param webLayout\n     */\n    protected DefaultWebCreator(@NonNull Activity activity, @Nullable ViewGroup viewGroup, ViewGroup.LayoutParams lp, int index, BaseIndicatorView progressView, WebView webView, IWebLayout webLayout) {\n        this.mActivity = activity;\n        this.mViewGroup = viewGroup;\n        this.mIsNeedDefaultProgress = false;\n        this.mIndex = index;\n        this.mLayoutParams = lp;\n        this.mProgressView = progressView;\n        this.mWebView = webView;\n        this.mIWebLayout = webLayout;\n    }\n\n\n    public void setWebView(WebView webView) {\n        mWebView = webView;\n    }\n\n    public FrameLayout getFrameLayout() {\n        return mFrameLayout;\n    }\n\n\n    public View getTargetProgress() {\n        return mTargetProgress;\n    }\n\n    public void setTargetProgress(View targetProgress) {\n        this.mTargetProgress = targetProgress;\n    }\n\n    @Override\n    public DefaultWebCreator create() {\n        if (mIsCreated) {\n            return this;\n        }\n        mIsCreated = true;\n        ViewGroup mViewGroup = this.mViewGroup;\n        if (mViewGroup == null) {\n            mViewGroup = this.mFrameLayout = (FrameLayout) createLayout();\n            mActivity.setContentView(mViewGroup);\n        } else {\n            if (mIndex == -1) {\n                mViewGroup.addView(this.mFrameLayout = (FrameLayout) createLayout(), mLayoutParams);\n            } else {\n                mViewGroup.addView(this.mFrameLayout = (FrameLayout) createLayout(), mIndex, mLayoutParams);\n            }\n        }\n        return this;\n    }\n\n    @Override\n    public WebView getWebView() {\n        return mWebView;\n    }\n\n    @Override\n    public FrameLayout getWebParentLayout() {\n        return mFrameLayout;\n    }\n\n    private ViewGroup createLayout() {\n        Activity mActivity = this.mActivity;\n        WebParentLayout mFrameLayout = new WebParentLayout(mActivity);\n        mFrameLayout.setId(R.id.web_parent_layout_id);\n        mFrameLayout.setBackgroundColor(Color.WHITE);\n        View target = mIWebLayout == null ? (this.mWebView = (WebView) createWebView()) : webLayout();\n        FrameLayout.LayoutParams mLayoutParams = new FrameLayout.LayoutParams(-1, -1);\n        mFrameLayout.addView(target, mLayoutParams);\n        mFrameLayout.bindWebView(this.mWebView);\n        LogUtils.i(TAG, \"  instanceof  AgentWebView:\" + (this.mWebView instanceof AgentWebView));\n        if (this.mWebView instanceof AgentWebView) {\n            AgentWebConfig.WEBVIEW_TYPE = AgentWebConfig.WEBVIEW_AGENTWEB_SAFE_TYPE;\n        }\n        ViewStub mViewStub = new ViewStub(mActivity);\n        mViewStub.setId(R.id.mainframe_error_viewsub_id);\n        mFrameLayout.addView(mViewStub, new FrameLayout.LayoutParams(-1, -1));\n        if (mIsNeedDefaultProgress) {\n            FrameLayout.LayoutParams lp = null;\n            WebIndicator mWebIndicator = new WebIndicator(mActivity);\n            if (mHeight > 0) {\n                lp = new FrameLayout.LayoutParams(-2, AgentWebUtils.dp2px(mActivity, mHeight));\n            } else {\n                lp = mWebIndicator.offerLayoutParams();\n            }\n            if (mColor != -1) {\n                mWebIndicator.setColor(mColor);\n            }\n            lp.gravity = Gravity.TOP;\n            mFrameLayout.addView((View) (this.mBaseIndicatorSpec = mWebIndicator), lp);\n            mWebIndicator.setVisibility(View.GONE);\n        } else if (!mIsNeedDefaultProgress && mProgressView != null) {\n            mFrameLayout.addView((View) (this.mBaseIndicatorSpec = (BaseIndicatorSpec) mProgressView), mProgressView.offerLayoutParams());\n            mProgressView.setVisibility(View.GONE);\n        }\n        return mFrameLayout;\n    }\n\n\n    private View webLayout() {\n        WebView mWebView = null;\n        if ((mWebView = mIWebLayout.getWebView()) == null) {\n            mWebView = createWebView();\n            mIWebLayout.getLayout().addView(mWebView, -1, -1);\n            LogUtils.i(TAG, \"add webview\");\n        } else {\n            AgentWebConfig.WEBVIEW_TYPE = AgentWebConfig.WEBVIEW_CUSTOM_TYPE;\n        }\n        this.mWebView = mWebView;\n        return mIWebLayout.getLayout();\n    }\n\n    private WebView createWebView() {\n        WebView mWebView = null;\n        if (this.mWebView != null) {\n            mWebView = this.mWebView;\n            AgentWebConfig.WEBVIEW_TYPE = AgentWebConfig.WEBVIEW_CUSTOM_TYPE;\n        } else if (AgentWebConfig.IS_KITKAT_OR_BELOW_KITKAT) {\n            mWebView = new AgentWebView(mActivity);\n            AgentWebConfig.WEBVIEW_TYPE = AgentWebConfig.WEBVIEW_AGENTWEB_SAFE_TYPE;\n        } else {\n            mWebView = new LollipopFixedWebView(mActivity);\n            AgentWebConfig.WEBVIEW_TYPE = AgentWebConfig.WEBVIEW_DEFAULT_TYPE;\n        }\n        return mWebView;\n    }\n\n    @Override\n    public BaseIndicatorSpec offer() {\n        return mBaseIndicatorSpec;\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/DefaultWebLifeCycleImpl.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.os.Build;\nimport android.webkit.WebView;\n\n/**\n * @author cenxiaozhong\n * @date 2017/6/3\n * @since 2.0.0\n */\npublic class DefaultWebLifeCycleImpl implements WebLifeCycle {\n    private WebView mWebView;\n    DefaultWebLifeCycleImpl(WebView webView) {\n        this.mWebView = webView;\n    }\n\n    @Override\n    public void onResume() {\n        if (this.mWebView != null) {\n            if (Build.VERSION.SDK_INT >= 11){\n                this.mWebView.onResume();\n            }\n            this.mWebView.resumeTimers();\n        }\n    }\n\n    @Override\n    public void onPause() {\n        if (this.mWebView != null) {\n            if (Build.VERSION.SDK_INT >= 11){\n                this.mWebView.onPause();\n            }\n            this.mWebView.pauseTimers();\n        }\n    }\n\n    @Override\n    public void onDestroy() {\n        if(this.mWebView!=null){\n            this.mWebView.resumeTimers();\n        }\n        AgentWebUtils.clearWebView(this.mWebView);\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/EventHandlerImpl.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.view.KeyEvent;\nimport android.webkit.WebView;\n\n/**\n * IEventHandler 对事件的处理，主要是针对\n * 视屏状态进行了处理 ， 如果当前状态为 视频状态\n * 则先退出视频。\n *\n * @author cenxiaozhong\n * @date 2017/6/3\n * @since 2.0.0\n */\npublic class EventHandlerImpl implements IEventHandler {\n\tprivate WebView mWebView;\n\tprivate EventInterceptor mEventInterceptor;\n\n\tpublic static final EventHandlerImpl getInstantce(WebView view, EventInterceptor eventInterceptor) {\n\t\treturn new EventHandlerImpl(view, eventInterceptor);\n\t}\n\n\tpublic EventHandlerImpl(WebView webView, EventInterceptor eventInterceptor) {\n\t\tthis.mWebView = webView;\n\t\tthis.mEventInterceptor = eventInterceptor;\n\t}\n\n\t@Override\n\tpublic boolean onKeyDown(int keyCode, KeyEvent event) {\n\t\tif (keyCode == KeyEvent.KEYCODE_BACK) {\n\t\t\treturn back();\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic boolean back() {\n\t\tif (this.mEventInterceptor != null && this.mEventInterceptor.event()) {\n\t\t\treturn true;\n\t\t}\n\t\tif (mWebView != null && mWebView.canGoBack()) {\n\t\t\tmWebView.goBack();\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/EventInterceptor.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\n/**\n * @author cenxiaozhong\n * @date 2017/6/3\n * @since 1.0.0\n */\npublic interface EventInterceptor {\n    boolean event();\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/HookManager.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\n\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\npublic class HookManager {\n\n    public static AgentWeb hookAgentWeb(AgentWeb agentWeb, AgentWeb.AgentBuilder agentBuilder) {\n        return agentWeb;\n    }\n\n    public static boolean permissionHook(String url,String[]permissions){\n        return true;\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/HttpHeaders.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\n\nimport androidx.collection.ArrayMap;\n\nimport java.util.Map;\n\n\n/**\n * @author cenxiaozhong\n * @date 2017/7/5\n * @since 2.0.0\n */\npublic class HttpHeaders {\n    public static HttpHeaders create() {\n        return new HttpHeaders();\n    }\n\n    private final Map<String, Map<String, String>> mHeaders;\n\n    HttpHeaders() {\n        mHeaders = new ArrayMap<String, Map<String, String>>();\n    }\n\n    public Map<String, String> getHeaders(String url) {\n        String subUrl = subBaseUrl(url);\n        if (mHeaders.get(subUrl) == null) {\n            Map<String, String> headers = new ArrayMap<>();\n            mHeaders.put(subUrl, headers);\n            return headers;\n        }\n        return mHeaders.get(subUrl);\n    }\n\n    public void additionalHttpHeader(String url, String k, String v) {\n        if (null == url) {\n            return;\n        }\n        url = subBaseUrl(url);\n        Map<String, Map<String, String>> mHeaders = getHeaders();\n        Map<String, String> headersMap = mHeaders.get(subBaseUrl(url));\n        if (null == headersMap) {\n            headersMap = new ArrayMap<>();\n        }\n        headersMap.put(k, v);\n        mHeaders.put(url, headersMap);\n    }\n\n\n    public void additionalHttpHeaders(String url, Map<String, String> headers) {\n        if (null == url) {\n            return;\n        }\n        String subUrl = subBaseUrl(url);\n        Map<String, Map<String, String>> mHeaders = getHeaders();\n        Map<String, String> headersMap = headers;\n        if (null == headersMap) {\n            headersMap = new ArrayMap<>();\n        }\n        mHeaders.put(subUrl, headersMap);\n    }\n\n    public void removeHttpHeader(String url, String k) {\n        if (null == url) {\n            return;\n        }\n        String subUrl = subBaseUrl(url);\n        Map<String, Map<String, String>> mHeaders = getHeaders();\n        Map<String, String> headersMap = mHeaders.get(subUrl);\n        if (null != headersMap) {\n            headersMap.remove(k);\n        }\n    }\n\n    public boolean isEmptyHeaders(String url) {\n        url = subBaseUrl(url);\n        Map<String, String> heads = getHeaders(url);\n        return heads == null || heads.isEmpty();\n    }\n\n    public Map<String, Map<String, String>> getHeaders() {\n        return this.mHeaders;\n    }\n\n    private String subBaseUrl(String originUrl) {\n        if (TextUtils.isEmpty(originUrl)) {\n            return originUrl;\n        }\n        Uri originUri = Uri.parse(originUrl);\n        return originUri.getScheme() + \"://\" + originUri.getAuthority();\n    }\n\n    @Override\n    public String toString() {\n        return \"HttpHeaders{\" +\n                \"mHeaders=\" + mHeaders +\n                '}';\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/IAgentWebSettings.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.webkit.WebView;\n\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\n\npublic interface IAgentWebSettings<T extends android.webkit.WebSettings> {\n\n    IAgentWebSettings toSetting(WebView webView);\n\n    T getWebSettings();\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/IEventHandler.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.view.KeyEvent;\n\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\npublic interface IEventHandler {\n\n    boolean onKeyDown(int keyCode, KeyEvent event);\n\n    boolean back();\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/IUrlLoader.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport java.util.Map;\n\n/**\n * @author cenxiaozhong\n * @date 2017/6/3\n * @update 4.0.0\n * @since 2.0.0\n */\npublic interface IUrlLoader {\n\n\n    void loadUrl(String url);\n\n    void loadUrl(String url, Map<String, String> headers);\n\n    void reload();\n\n    void loadData(String data, String mimeType, String encoding);\n\n    void stopLoading();\n\n    void loadDataWithBaseURL(String baseUrl, String data,\n                             String mimeType, String encoding, String historyUrl);\n\n    void postUrl(String url, byte[] params);\n\n    HttpHeaders getHttpHeaders();\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/IVideo.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.view.View;\nimport android.webkit.WebChromeClient;\n\n\n/**\n * @author cenxiaozhong\n * @date 2017/6/10\n * @since 2.0.0\n */\npublic interface IVideo {\n\n\n    void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback);\n\n\n    void onHideCustomView();\n\n\n    boolean isVideoState();\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/IWebIndicator.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\n\n\npublic interface IWebIndicator<T extends BaseIndicatorSpec> {\n\n\n    T offer();\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/IWebLayout.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.view.ViewGroup;\nimport android.webkit.WebView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\n/**\n * Created by cenxiaozhong on 2017/7/1.\n */\n/**\n * @author cenxiaozhong\n * @date 2017/7/1\n * @update 4.0.0\n * @since 1.0.0\n */\npublic interface IWebLayout<T extends WebView,V extends ViewGroup> {\n\n    /**\n     *\n     * @return WebView 的父控件\n     */\n    @NonNull\n    V getLayout();\n\n    /**\n     *\n     * @return 返回 WebView  或 WebView 的子View ，返回null AgentWeb 内部会创建适当 WebView\n     */\n    @Nullable\n    T getWebView();\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/IndicatorController.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.webkit.WebView;\n/**\n * @author cenxiaozhong\n * @update 4.0.0\n * @since 1.0.0\n */\n\npublic interface IndicatorController {\n\n    void progress(WebView v, int newProgress);\n\n    BaseIndicatorSpec offerIndicator();\n\n    void showIndicator();\n\n    void setProgress(int newProgress);\n\n    void finish();\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/IndicatorHandler.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.webkit.WebView;\n\n\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\npublic class IndicatorHandler implements IndicatorController {\n\tprivate BaseIndicatorSpec mBaseIndicatorSpec;\n\n\t@Override\n\tpublic void progress(WebView v, int newProgress) {\n\n\t\tif (newProgress == 0) {\n\t\t\treset();\n\t\t} else if (newProgress > 0 && newProgress <= 10) {\n\t\t\tshowIndicator();\n\t\t} else if (newProgress > 10 && newProgress < 95) {\n\t\t\tsetProgress(newProgress);\n\t\t} else {\n\t\t\tsetProgress(newProgress);\n\t\t\tfinish();\n\t\t}\n\n\t}\n\n\t@Override\n\tpublic BaseIndicatorSpec offerIndicator() {\n\t\treturn this.mBaseIndicatorSpec;\n\t}\n\n\tpublic void reset() {\n\n\t\tif (mBaseIndicatorSpec != null) {\n\t\t\tmBaseIndicatorSpec.reset();\n\t\t}\n\t}\n\n\t@Override\n\tpublic void finish() {\n\t\tif (mBaseIndicatorSpec != null) {\n\t\t\tmBaseIndicatorSpec.hide();\n\t\t}\n\t}\n\n\t@Override\n\tpublic void setProgress(int n) {\n\t\tif (mBaseIndicatorSpec != null) {\n\t\t\tmBaseIndicatorSpec.setProgress(n);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void showIndicator() {\n\n\t\tif (mBaseIndicatorSpec != null) {\n\t\t\tmBaseIndicatorSpec.show();\n\t\t}\n\t}\n\n\tstatic IndicatorHandler getInstance() {\n\t\treturn new IndicatorHandler();\n\t}\n\n\n\tIndicatorHandler inJectIndicator(BaseIndicatorSpec baseIndicatorSpec) {\n\t\tthis.mBaseIndicatorSpec = baseIndicatorSpec;\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/JsAccessEntrace.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.webkit.ValueCallback;\n\n/**\n * @author cenxiaozhong\n * @date 2017/5/14\n * @since 1.0.0\n */\npublic interface JsAccessEntrace extends QuickCallJs {\n\n\n    void callJs(String js, ValueCallback<String> callback);\n\n    void callJs(String js);\n\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/JsAccessEntraceImpl.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.webkit.ValueCallback;\nimport android.webkit.WebView;\n\n\n/**\n * @author cenxiaozhong\n * @date 2017/6/3\n * @since 1.0.0\n */\npublic class JsAccessEntraceImpl extends BaseJsAccessEntrace {\n\n    private WebView mWebView;\n    private Handler mHandler = new Handler(Looper.getMainLooper());\n\n    public static JsAccessEntraceImpl getInstance(WebView webView) {\n        return new JsAccessEntraceImpl(webView);\n    }\n\n    private JsAccessEntraceImpl(WebView webView) {\n        super(webView);\n        this.mWebView = webView;\n    }\n\n    private void safeCallJs(final String s, final ValueCallback valueCallback) {\n        mHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                callJs(s, valueCallback);\n            }\n        });\n    }\n\n    @Override\n    public void callJs(String params, final ValueCallback<String> callback) {\n        if (Thread.currentThread() != Looper.getMainLooper().getThread()) {\n            safeCallJs(params, callback);\n            return;\n        }\n        super.callJs(params,callback);\n    }\n\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/JsBaseInterfaceHolder.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.os.Build;\nimport android.webkit.JavascriptInterface;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Method;\n\n/**\n * @author cenxiaozhong\n * @date 2017/5/13\n * @since 1.0.0\n */\npublic abstract class JsBaseInterfaceHolder implements JsInterfaceHolder {\n\n\tprivate AgentWeb.SecurityType mSecurityType;\n\n\tprotected JsBaseInterfaceHolder(AgentWeb.SecurityType securityType) {\n\t\tthis.mSecurityType = securityType;\n\t}\n\n\t@Override\n\tpublic boolean checkObject(Object v) {\n\t\tif (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {\n\t\t\treturn true;\n\t\t}\n\t\tif (AgentWebConfig.WEBVIEW_TYPE == AgentWebConfig.WEBVIEW_AGENTWEB_SAFE_TYPE) {\n\t\t\treturn true;\n\t\t}\n\t\tboolean tag = false;\n\t\tClass clazz = v.getClass();\n\t\tMethod[] mMethods = clazz.getMethods();\n\t\tfor (Method mMethod : mMethods) {\n\t\t\tAnnotation[] mAnnotations = mMethod.getAnnotations();\n\t\t\tfor (Annotation mAnnotation : mAnnotations) {\n\t\t\t\tif (mAnnotation instanceof JavascriptInterface) {\n\t\t\t\t\ttag = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (tag) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn tag;\n\t}\n\n\tprotected boolean checkSecurity() {\n\t\treturn mSecurityType != AgentWeb.SecurityType.STRICT_CHECK\n\t\t\t\t? true : AgentWebConfig.WEBVIEW_TYPE == AgentWebConfig.WEBVIEW_AGENTWEB_SAFE_TYPE\n\t\t\t\t? true : Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1;\n\t}\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/JsCallJava.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.text.TextUtils;\nimport android.util.Log;\nimport android.webkit.WebView;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.lang.reflect.Method;\nimport java.util.HashMap;\n\npublic class JsCallJava {\n    private final static String TAG = \"JsCallJava\";\n    private final static String RETURN_RESULT_FORMAT = \"{\\\"CODE\\\": %d, \\\"result\\\": %s}\";\n    private static final String MSG_PROMPT_HEADER = \"AgentWeb:\";\n    private static final String KEY_OBJ = \"obj\";\n    private static final String KEY_METHOD = \"method\";\n    private static final String KEY_TYPES = \"types\";\n    private static final String KEY_ARGS = \"args\";\n    private static final String[] IGNORE_UNSAFE_METHODS = {\"getClass\", \"hashCode\", \"notify\", \"notifyAll\", \"equals\", \"toString\", \"wait\"};\n    private HashMap<String, Method> mMethodsMap;\n    private Object mInterfaceObj;\n    private String mInterfacedName;\n    private String mPreloadInterfaceJs;\n\n    public JsCallJava(Object interfaceObj, String interfaceName) {\n        try {\n            if (TextUtils.isEmpty(interfaceName)) {\n                throw new Exception(\"injected name can not be null\");\n            }\n            mInterfaceObj = interfaceObj;\n            mInterfacedName = interfaceName;\n            mMethodsMap = new HashMap<String, Method>();\n            // getMethods会获得所有继承与非继承的方法\n            Method[] methods = mInterfaceObj.getClass().getMethods();\n            // 拼接的js脚本可参照备份文件：./library/doc/injected.js\n            StringBuilder sb = new StringBuilder(\"javascript:(function(b){console.log(\\\"\");\n            sb.append(mInterfacedName);\n            sb.append(\" init begin\\\");var a={queue:[],callback:function(){var d=Array.prototype.slice.call(arguments,0);var c=d.shift();var e=d.shift();this.queue[c].apply(this,d);if(!e){delete this.queue[c]}}};\");\n            for (Method method : methods) {\n                Log.i(\"Info\",\"method:\"+method);\n                String sign;\n                if ((sign = genJavaMethodSign(method)) == null) {\n                    continue;\n                }\n                mMethodsMap.put(sign, method);\n                sb.append(String.format(\"a.%s=\", method.getName()));\n            }\n            sb.append(\"function(){var f=Array.prototype.slice.call(arguments,0);if(f.length<1){throw\\\"\");\n            sb.append(mInterfacedName);\n            sb.append(\" call result, message:miss method name\\\"}var e=[];for(var h=1;h<f.length;h++){var c=f[h];var j=typeof c;e[e.length]=j;if(j==\\\"function\\\"){var d=a.queue.length;a.queue[d]=c;f[h]=d}}var k = new Date().getTime();var l = f.shift();var m=prompt('\");\n            sb.append(MSG_PROMPT_HEADER);\n            sb.append(\"'+JSON.stringify(\");\n            sb.append(promptMsgFormat(\"'\" + mInterfacedName + \"'\", \"l\", \"e\", \"f\"));\n            sb.append(\"));console.log(\\\"invoke \\\"+l+\\\", time: \\\"+(new Date().getTime()-k));var g=JSON.parse(m);if(g.CODE!=200){throw\\\"\");\n            sb.append(mInterfacedName);\n            sb.append(\" call result, CODE:\\\"+g.CODE+\\\", message:\\\"+g.result}return g.result};Object.getOwnPropertyNames(a).forEach(function(d){var c=a[d];if(typeof c===\\\"function\\\"&&d!==\\\"callback\\\"){a[d]=function(){return c.apply(a,[d].concat(Array.prototype.slice.call(arguments,0)))}}});b.\");\n            sb.append(mInterfacedName);\n            sb.append(\"=a;console.log(\\\"\");\n            sb.append(mInterfacedName);\n            sb.append(\" init end\\\")})(window)\");\n            mPreloadInterfaceJs = sb.toString();\n            sb.setLength(0);\n        } catch (Exception e) {\n            if (LogUtils.isDebug()) {\n                Log.e(TAG, \"init js result:\" + e.getMessage());\n            }\n        }\n    }\n\n    private String genJavaMethodSign(Method method) {\n        String sign = method.getName();\n        Class[] argsTypes = method.getParameterTypes();\n        for (String ignoreMethod : IGNORE_UNSAFE_METHODS) {\n            if (ignoreMethod.equals(sign)) {\n                if (LogUtils.isDebug()) {\n                    Log.w(TAG, \"method(\" + sign + \") is unsafe, will be pass\");\n                }\n                return null;\n            }\n        }\n        int len = argsTypes.length;\n        for (int k = 0; k < len; k++) {\n            Class cls = argsTypes[k];\n            if (cls == String.class) {\n                sign += \"_S\";\n            } else if (cls == int.class ||\n                    cls == long.class ||\n                    cls == float.class ||\n                    cls == double.class) {\n                sign += \"_N\";\n            } else if (cls == boolean.class) {\n                sign += \"_B\";\n            } else if (cls == JSONObject.class) {\n                sign += \"_O\";\n            } else if (cls == JsCallback.class) {\n                sign += \"_F\";\n            } else {\n                sign += \"_P\";\n            }\n        }\n        return sign;\n    }\n\n    public String getPreloadInterfaceJs() {\n        return mPreloadInterfaceJs;\n    }\n\n    public String call(WebView webView, JSONObject jsonObject) {\n        long time = 0;\n        if (LogUtils.isDebug()) {\n            time = android.os.SystemClock.uptimeMillis();\n        }\n        if (jsonObject != null) {\n            try {\n                String methodName = jsonObject.getString(KEY_METHOD);\n                JSONArray argsTypes = jsonObject.getJSONArray(KEY_TYPES);\n                JSONArray argsVals = jsonObject.getJSONArray(KEY_ARGS);\n                String sign = methodName;\n                int len = argsTypes.length();\n                Object[] values = new Object[len];\n                int numIndex = 0;\n                String currType;\n\n                for (int k = 0; k < len; k++) {\n                    currType = argsTypes.optString(k);\n                    if (\"string\".equals(currType)) {\n                        sign += \"_S\";\n                        values[k] = argsVals.isNull(k) ? null : argsVals.getString(k);\n                    } else if (\"number\".equals(currType)) {\n                        sign += \"_N\";\n                        numIndex = numIndex * 10 + k + 1;\n                    } else if (\"boolean\".equals(currType)) {\n                        sign += \"_B\";\n                        values[k] = argsVals.getBoolean(k);\n                    } else if (\"object\".equals(currType)) {\n                        sign += \"_O\";\n                        values[k] = argsVals.isNull(k) ? null : argsVals.getJSONObject(k);\n                    } else if (\"function\".equals(currType)) {\n                        sign += \"_F\";\n                        values[k] = new JsCallback(webView, mInterfacedName, argsVals.getInt(k));\n                    } else {\n                        sign += \"_P\";\n                    }\n                }\n\n                Method currMethod = mMethodsMap.get(sign);\n\n                // 方法匹配失败\n                if (currMethod == null) {\n                    return getReturn(jsonObject, 500, \"not found method(\" + sign + \") with valid parameters\", time);\n                }\n                // 数字类型细分匹配\n                if (numIndex > 0) {\n                    Class[] methodTypes = currMethod.getParameterTypes();\n                    int currIndex;\n                    Class currCls;\n                    while (numIndex > 0) {\n                        currIndex = numIndex - numIndex / 10 * 10 - 1;\n                        currCls = methodTypes[currIndex];\n                        if (currCls == int.class) {\n                            values[currIndex] = argsVals.getInt(currIndex);\n                        } else if (currCls == long.class) {\n                            //WARN: argsJson.getLong(k + defValue) will return a bigger incorrect number\n                            values[currIndex] = Long.parseLong(argsVals.getString(currIndex));\n                        } else {\n                            values[currIndex] = argsVals.getDouble(currIndex);\n                        }\n                        numIndex /= 10;\n                    }\n                }\n\n                return getReturn(jsonObject, 200, currMethod.invoke(mInterfaceObj, values), time);\n            } catch (Exception e) {\n                LogUtils.safeCheckCrash(TAG, \"call\", e);\n                //优先返回详细的错误信息\n                if (e.getCause() != null) {\n                    return getReturn(jsonObject, 500, \"method execute result:\" + e.getCause().getMessage(), time);\n                }\n                return getReturn(jsonObject, 500, \"method execute result:\" + e.getMessage(), time);\n            }\n        } else {\n            return getReturn(jsonObject, 500, \"call data empty\", time);\n        }\n    }\n\n    private String getReturn(JSONObject reqJson, int stateCode, Object result, long time) {\n        String insertRes;\n        if (result == null) {\n            insertRes = \"null\";\n        } else if (result instanceof String) {\n            result = ((String) result).replace(\"\\\"\", \"\\\\\\\"\");\n            insertRes = \"\\\"\".concat(String.valueOf(result)).concat(\"\\\"\");\n        } else { // 其他类型直接转换\n            insertRes = String.valueOf(result);\n\n            // 兼容：如果在解决WebView注入安全漏洞时，js注入采用的是XXX:function(){return prompt(...)}的形式，函数返回类型包括：void、int、boolean、String；\n            // 在返回给网页（onJsPrompt方法中jsPromptResult.confirm）的时候强制返回的是String类型，所以在此将result的值加双引号兼容一下；\n            // insertRes = \"\\\"\".concat(String.valueOf(result)).concat(\"\\\"\");\n        }\n        String resStr = String.format(RETURN_RESULT_FORMAT, stateCode, insertRes);\n        if (LogUtils.isDebug()) {\n            Log.d(TAG, \"call time: \" + (android.os.SystemClock.uptimeMillis() - time) + \", request: \" + reqJson + \", result:\" + resStr);\n        }\n        return resStr;\n    }\n\n    private static String promptMsgFormat(String object, String method, String types, String args) {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"{\");\n        sb.append(KEY_OBJ).append(\":\").append(object).append(\",\");\n        sb.append(KEY_METHOD).append(\":\").append(method).append(\",\");\n        sb.append(KEY_TYPES).append(\":\").append(types).append(\",\");\n        sb.append(KEY_ARGS).append(\":\").append(args);\n        sb.append(\"}\");\n        return sb.toString();\n    }\n\n    /**\n     * 是否是“Java接口类中方法调用”的内部消息；\n     *\n     * @param message\n     * @return\n     */\n    static boolean isSafeWebViewCallMsg(String message) {\n        return message.startsWith(MSG_PROMPT_HEADER);\n    }\n\n    static JSONObject getMsgJSONObject(String message) {\n        message = message.substring(MSG_PROMPT_HEADER.length());\n        JSONObject jsonObject;\n        try {\n            jsonObject = new JSONObject(message);\n        } catch (JSONException e) {\n            e.printStackTrace();\n            jsonObject = new JSONObject();\n        }\n        return jsonObject;\n    }\n\n    static String getInterfacedName(JSONObject jsonObject) {\n        return jsonObject.optString(KEY_OBJ);\n    }\n}"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/JsCallback.java",
    "content": "\n\n/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.util.Log;\nimport android.webkit.WebView;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.lang.ref.WeakReference;\npublic class JsCallback {\n    private static final String CALLBACK_JS_FORMAT = \"javascript:%s.callback(%d, %d %s);\";\n    private int mIndex;\n    private boolean mCouldGoOn;\n    private WeakReference<WebView> mWebViewRef;\n    private int mIsPermanent;\n    private String mInjectedName;\n\n    public JsCallback(WebView view, String injectedName, int index) {\n        mCouldGoOn = true;\n        mWebViewRef = new WeakReference<WebView>(view);\n        mInjectedName = injectedName;\n        mIndex = index;\n    }\n\n    /**\n     * 向网页执行js回调；\n     * @param args\n     * @throws JsCallbackException\n     */\n    public void apply (Object... args) throws JsCallbackException {\n        if (mWebViewRef.get() == null) {\n            throw new JsCallbackException(\"the WebView related to the JsCallback has been recycled\");\n        }\n        if (!mCouldGoOn) {\n            throw new JsCallbackException(\"the JsCallback isn't permanent,cannot be called more than once\");\n        }\n        StringBuilder sb = new StringBuilder();\n        for (Object arg : args){\n            sb.append(\",\");\n            boolean isStrArg = arg instanceof String;\n            // 有的接口将Json对象转换成了String返回，这里不能加双引号，否则网页会认为是String而不是JavaScript对象；\n            boolean isObjArg = isJavaScriptObject(arg);\n            if (isStrArg && !isObjArg) {\n                sb.append(\"\\\"\");\n            }\n            sb.append(String.valueOf(arg));\n            if (isStrArg && !isObjArg) {\n                sb.append(\"\\\"\");\n            }\n        }\n        String execJs = String.format(CALLBACK_JS_FORMAT, mInjectedName, mIndex, mIsPermanent, sb.toString());\n        if (LogUtils.isDebug()) {\n            Log.d(\"JsCallBack\", execJs);\n        }\n        mWebViewRef.get().loadUrl(execJs);\n        mCouldGoOn = mIsPermanent > 0;\n    }\n\n    /**\n     * 是否是JSON(JavaScript Object Notation)对象；\n     * @param obj\n     * @return\n     */\n    private boolean isJavaScriptObject(Object obj) {\n        if (obj instanceof JSONObject || obj instanceof JSONArray) {\n            return true;\n        } else {\n            String json = obj.toString();\n            try {\n                new JSONObject(json);\n            } catch (JSONException e) {\n                try {\n                    new JSONArray(json);\n                } catch (JSONException e1) {\n                    return false;\n                }\n            }\n            return true;\n        }\n    }\n\n    /**\n     * 一般传入到Java方法的js function是一次性使用的，即在Java层jsCallback.apply(...)之后不能再发起回调了；\n     * 如果需要传入的function能够在当前页面生命周期内多次使用，请在第一次apply前setPermanent(true)；\n     * @param value\n     */\n    public void setPermanent (boolean value) {\n        mIsPermanent = value ? 1 : 0;\n    }\n\n    public static class JsCallbackException extends Exception {\n        public JsCallbackException (String msg) {\n            super(msg);\n        }\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/JsInterfaceHolder.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport java.util.Map;\n\n/**\n * @author cenxiaozhong\n * @date 2017/5/13\n * @since 1.0.0\n */\npublic interface JsInterfaceHolder {\n\n    JsInterfaceHolder addJavaObjects(Map<String, Object> maps);\n\n    JsInterfaceHolder addJavaObject(String k, Object v);\n\n    boolean checkObject(Object v);\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/JsInterfaceHolderImpl.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.webkit.WebView;\n\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * @author cenxiaozhong\n * @date 2017/5/13\n * @since 1.0.0\n */\npublic class JsInterfaceHolderImpl extends JsBaseInterfaceHolder {\n\n\tprivate static final String TAG = JsInterfaceHolderImpl.class.getSimpleName();\n\tprivate WebView mWebView;\n\tprivate AgentWeb.SecurityType mSecurityType;\n\n\tstatic JsInterfaceHolderImpl getJsInterfaceHolder(WebView webView, AgentWeb.SecurityType securityType) {\n\t\treturn new JsInterfaceHolderImpl(webView, securityType);\n\t}\n\n\tJsInterfaceHolderImpl(WebView webView, AgentWeb.SecurityType securityType) {\n\t\tsuper(securityType);\n\t\tthis.mWebView = webView;\n\t\tthis.mSecurityType = securityType;\n\t}\n\n\t@Override\n\tpublic JsInterfaceHolder addJavaObjects(Map<String, Object> maps) {\n\t\tif (!checkSecurity()) {\n\t\t\tLogUtils.e(TAG, \"The injected object is not safe, give up injection\");\n\t\t\treturn this;\n\t\t}\n\t\tSet<Map.Entry<String, Object>> sets = maps.entrySet();\n\t\tfor (Map.Entry<String, Object> mEntry : sets) {\n\t\t\tObject v = mEntry.getValue();\n\t\t\tboolean t = checkObject(v);\n\t\t\tif (!t) {\n\t\t\t\tthrow new JsInterfaceObjectException(\"This object has not offer method javascript to call ,please check addJavascriptInterface annotation was be added\");\n\t\t\t} else {\n\t\t\t\taddJavaObjectDirect(mEntry.getKey(), v);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic JsInterfaceHolder addJavaObject(String k, Object v) {\n\t\tif (!checkSecurity()) {\n\t\t\treturn this;\n\t\t}\n\t\tboolean t = checkObject(v);\n\t\tif (!t) {\n\t\t\tthrow new JsInterfaceObjectException(\"this object has not offer method javascript to call , please check addJavascriptInterface annotation was be added\");\n\t\t} else {\n\t\t\taddJavaObjectDirect(k, v);\n\t\t}\n\t\treturn this;\n\t}\n\n\tprivate JsInterfaceHolder addJavaObjectDirect(String k, Object v) {\n\t\tLogUtils.i(TAG, \"k:\" + k + \"  v:\" + v);\n\t\tthis.mWebView.addJavascriptInterface(v, k);\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/JsInterfaceObjectException.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\n\n/**\n * @author cenxiaozhong\n * @date 2017/5/13\n * @since 1.0.0\n */\npublic class JsInterfaceObjectException extends RuntimeException {\n    JsInterfaceObjectException(String msg){\n        super(msg);\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/LayoutParamsOffer.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.widget.FrameLayout;\n\n/**\n * @author cenxiaozhong\n * @date 2017/5/12\n * @since 1.0.0\n */\npublic interface LayoutParamsOffer<T extends FrameLayout.LayoutParams> {\n\n    T offerLayoutParams();\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/LogUtils.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.util.Log;\n\n/**\n * @author cenxiaozhong\n * @date 2017/5/28\n * @since 1.0.0\n */\npublic class LogUtils {\n\n    private static final String PREFIX = \" agentweb - \";\n\n    public static boolean isDebug() {\n        return AgentWebConfig.DEBUG;\n    }\n\n    public static void i(String tag, String message) {\n\n        if (isDebug()){\n            Log.i(PREFIX.concat(tag), message);\n        }\n    }\n\n    public static void v(String tag, String message) {\n\n        if (isDebug()){\n            Log.v(PREFIX.concat(tag), message);\n        }\n\n    }\n\n    public static void safeCheckCrash(String tag, String msg, Throwable tr) {\n        if (isDebug()) {\n            throw new RuntimeException(PREFIX.concat(tag) + \" \" + msg, tr);\n        } else {\n            Log.e(PREFIX.concat(tag), msg, tr);\n        }\n    }\n\n    public static void e(String tag, String msg, Throwable tr) {\n        Log.e(tag, msg, tr);\n    }\n\n    public static void e(String tag, String message) {\n\n        if (isDebug()){\n            Log.e(PREFIX.concat(tag), message);\n        }\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/LollipopFixedWebView.java",
    "content": "package com.just.agentweb;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.webkit.WebView;\n\n/**\n * 修复 Android 5.0 & 5.1 打开 WebView 闪退问题：\n * 参阅 https://stackoverflow.com/questions/41025200/android-view-inflateexception-error-inflating-class-android-webkit-webview\n */\n@SuppressWarnings(\"unused\")\npublic class LollipopFixedWebView extends WebView {\n    public LollipopFixedWebView(Context context) {\n        super(getFixedContext(context));\n    }\n\n    public LollipopFixedWebView(Context context, AttributeSet attrs) {\n        super(getFixedContext(context), attrs);\n    }\n\n    public LollipopFixedWebView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(getFixedContext(context), attrs, defStyleAttr);\n    }\n\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    public LollipopFixedWebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n        super(getFixedContext(context), attrs, defStyleAttr, defStyleRes);\n    }\n\n    public LollipopFixedWebView(Context context, AttributeSet attrs, int defStyleAttr, boolean privateBrowsing) {\n        super(getFixedContext(context), attrs, defStyleAttr, privateBrowsing);\n    }\n\n    public static Context getFixedContext(Context context) {\n//        if (Build.VERSION.SDK_INT >= 21 && Build.VERSION.SDK_INT < 23) {\n//            // A void crashing on Android 5 and 6 (API level 21 to 23)\n//            return context.createConfigurationContext(new Configuration());\n//        }\n        return context;\n    }\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/MiddlewareWebChromeBase.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.webkit.WebChromeClient;\n\n/**\n * @author cenxiaozhong\n * @date 2017/12/16\n * @since 3.0.0\n */\npublic class MiddlewareWebChromeBase extends WebChromeClientDelegate {\n\n    private MiddlewareWebChromeBase mMiddlewareWebChromeBase;\n\n    protected MiddlewareWebChromeBase(WebChromeClient webChromeClient) {\n        super(webChromeClient);\n    }\n\n    protected MiddlewareWebChromeBase() {\n        super(null);\n    }\n\n    @Override\n    final void setDelegate(WebChromeClient delegate) {\n        super.setDelegate(delegate);\n    }\n\n    final MiddlewareWebChromeBase enq(MiddlewareWebChromeBase middlewareWebChromeBase) {\n        setDelegate(middlewareWebChromeBase);\n        this.mMiddlewareWebChromeBase = middlewareWebChromeBase;\n        return this.mMiddlewareWebChromeBase;\n    }\n\n\n    final MiddlewareWebChromeBase next() {\n        return this.mMiddlewareWebChromeBase;\n    }\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/MiddlewareWebClientBase.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.webkit.WebViewClient;\n\n/**\n * @author cenxiaozhong\n * @date 2017/12/15\n * @since 3.0.0\n */\npublic class MiddlewareWebClientBase extends WebViewClientDelegate {\n    private MiddlewareWebClientBase mMiddleWrareWebClientBase;\n    private static String TAG = MiddlewareWebClientBase.class.getSimpleName();\n\n    MiddlewareWebClientBase(MiddlewareWebClientBase client) {\n        super(client);\n        this.mMiddleWrareWebClientBase = client;\n    }\n\n    protected MiddlewareWebClientBase(WebViewClient client) {\n        super(client);\n    }\n\n    protected MiddlewareWebClientBase() {\n        super(null);\n    }\n\n    final MiddlewareWebClientBase next() {\n        return this.mMiddleWrareWebClientBase;\n    }\n\n    @Override\n    final void setDelegate(WebViewClient delegate) {\n        super.setDelegate(delegate);\n\n    }\n\n    final MiddlewareWebClientBase enq(MiddlewareWebClientBase middleWrareWebClientBase) {\n        setDelegate(middleWrareWebClientBase);\n        this.mMiddleWrareWebClientBase = middleWrareWebClientBase;\n        return middleWrareWebClientBase;\n    }\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/NestedScrollAgentWebView.java",
    "content": "/*\n * Copyright (C)  LeonDevLifeLog(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.just.agentweb;\n\nimport android.content.Context;\nimport android.view.MotionEvent;\n\nimport androidx.core.view.NestedScrollingChild;\nimport androidx.core.view.NestedScrollingChildHelper;\nimport androidx.core.view.ViewCompat;\n\n/**\n * 结合CoordinatorLayout可以与Toolbar联动的webview\n * @author LeonDevLifeLog\n * @since 4.0.0\n */\n\npublic class NestedScrollAgentWebView extends AgentWebView implements NestedScrollingChild {\n\n    /***\n     * 方法一\n     * https://github.com/fashare2015/NestedScrollWebView\n     */\n    public NestedScrollAgentWebView(Context context) {\n        super(context);\n        initView();\n    }\n\n    private static final int INVALID_POINTER = -1;\n\n    private void initView() {\n        mChildHelper = new NestedScrollingChildHelper(this);\n        setNestedScrollingEnabled(true);\n    }\n\n\n    /**\n     * Position of the last motion event.\n     */\n    private int mLastMotionY;\n\n    /**\n     * ID of the active pointer. This is used to retain consistency during\n     * drags/flings if multiple pointers are used.\n     */\n    private int mActivePointerId = INVALID_POINTER;\n\n    /**\n     * Used during scrolling to retrieve the new offset within the window.\n     */\n    private final int[] mScrollOffset = new int[2];\n    private final int[] mScrollConsumed = new int[2];\n    private NestedScrollingChildHelper mChildHelper;\n    boolean mIsBeingDragged;\n\n\n    @Override\n    public boolean onTouchEvent(MotionEvent ev) {\n        final int actionMasked = ev.getActionMasked();\n\n        switch (actionMasked) {\n            case MotionEvent.ACTION_DOWN:\n                mIsBeingDragged = false;\n\n                // Remember where the motion event started\n                mLastMotionY = (int) ev.getY();\n//                downY = (int) ev.getY();\n\n                mActivePointerId = ev.getPointerId(0);\n                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);\n                break;\n\n            case MotionEvent.ACTION_MOVE:\n//                KLog.e(\"移动开始：\");\n                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);\n                if (activePointerIndex == -1) {\n                    break;\n                }\n//                if( !onlyVerticalMove(ev) ){\n//                    break;\n//                }\n\n                final int y = (int) ev.getY(activePointerIndex);\n                int deltaY = mLastMotionY - y;\n                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {\n                    deltaY -= mScrollConsumed[1];\n                }\n                // Scroll to follow the motion event\n                mLastMotionY = y - mScrollOffset[1];\n\n                final int oldY = getScrollY();\n                final int scrolledDeltaY = Math.max(0, oldY + deltaY) - oldY;\n                final int unconsumedY = deltaY - scrolledDeltaY;\n                if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {\n                    mLastMotionY -= mScrollOffset[1];\n                }\n//                KLog.e(\"移动结束：\");\n                break;\n            case MotionEvent.ACTION_UP:\n                mActivePointerId = INVALID_POINTER;\n                endDrag();\n                break;\n            case MotionEvent.ACTION_CANCEL:\n                mActivePointerId = INVALID_POINTER;\n                endDrag();\n                break;\n            case MotionEvent.ACTION_POINTER_DOWN: {\n                final int index = ev.getActionIndex();\n                mLastMotionY = (int) ev.getY(index);\n                mActivePointerId = ev.getPointerId(index);\n                break;\n            }\n            case MotionEvent.ACTION_POINTER_UP:\n                onSecondaryPointerUp(ev);\n                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));\n                break;\n            default:\n                break;\n        }\n        return super.onTouchEvent(ev);\n    }\n\n    private void endDrag() {\n        mIsBeingDragged = false;\n        stopNestedScroll();\n    }\n\n    private void onSecondaryPointerUp(MotionEvent ev) {\n        final int pointerIndex = ev.getActionIndex();\n        final int pointerId = ev.getPointerId(pointerIndex);\n        if (pointerId == mActivePointerId) {\n            // This was our active pointer going up. Choose a new\n            // active pointer and adjust accordingly.\n            // TODO: Make this decision more intelligent.\n            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;\n            mLastMotionY = (int) ev.getY(newPointerIndex);\n            mActivePointerId = ev.getPointerId(newPointerIndex);\n        }\n    }\n\n\n    @Override\n    public void setNestedScrollingEnabled(boolean enabled) {\n        mChildHelper.setNestedScrollingEnabled(enabled);\n    }\n\n    @Override\n    public boolean isNestedScrollingEnabled() {\n        return mChildHelper.isNestedScrollingEnabled();\n    }\n\n    @Override\n    public boolean startNestedScroll(int axes) {\n        return mChildHelper.startNestedScroll(axes);\n    }\n\n    @Override\n    public void stopNestedScroll() {\n        mChildHelper.stopNestedScroll();\n    }\n\n    @Override\n    public boolean hasNestedScrollingParent() {\n        return mChildHelper.hasNestedScrollingParent();\n    }\n\n    @Override\n    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {\n//        KLog.e(\"分配滚动事件：\" + dxConsumed + \"  \" + dyConsumed );\n        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);\n    }\n\n    @Override\n    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {\n        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);\n    }\n\n    @Override\n    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {\n        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);\n    }\n\n    @Override\n    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {\n        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/PermissionInterceptor.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\n/**\n * @author cenxiaozhong\n * @since 3.0.0\n */\npublic interface PermissionInterceptor {\n    boolean intercept(String url, String[] permissions, String action);\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/ProcessUtils.java",
    "content": "package com.just.agentweb;\n\nimport android.app.ActivityManager;\nimport android.app.Application;\nimport android.content.Context;\nimport android.text.TextUtils;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileReader;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.util.List;\n\n/**\n * Adapted from com.blankj.utilcode.util.ProcessUtils#getCurrentProcessName\n */\nclass ProcessUtils {\n\n    static String getCurrentProcessName(Context context) {\n        String name = getCurrentProcessNameByFile();\n        if (!TextUtils.isEmpty(name)) return name;\n        name = getCurrentProcessNameByAms(context);\n        if (!TextUtils.isEmpty(name)) return name;\n        name = getCurrentProcessNameByReflect(context);\n        return name;\n    }\n\n    private static String getCurrentProcessNameByFile() {\n        try {\n            File file = new File(\"/proc/\" + android.os.Process.myPid() + \"/\" + \"cmdline\");\n            BufferedReader mBufferedReader = new BufferedReader(new FileReader(file));\n            String processName = mBufferedReader.readLine().trim();\n            mBufferedReader.close();\n            return processName;\n        } catch (Exception e) {\n            e.printStackTrace();\n            return \"\";\n        }\n    }\n\n    private static String getCurrentProcessNameByAms(Context context) {\n        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);\n        if (am == null) return \"\";\n        List<ActivityManager.RunningAppProcessInfo> info = am.getRunningAppProcesses();\n        if (info == null || info.size() == 0) return \"\";\n        int pid = android.os.Process.myPid();\n        for (ActivityManager.RunningAppProcessInfo aInfo : info) {\n            if (aInfo.pid == pid) {\n                if (aInfo.processName != null) {\n                    return aInfo.processName;\n                }\n            }\n        }\n        return \"\";\n    }\n\n    private static String getCurrentProcessNameByReflect(Context context) {\n        String processName = \"\";\n        try {\n            Application app = (Application) context.getApplicationContext();\n            Field loadedApkField = app.getClass().getField(\"mLoadedApk\");\n            loadedApkField.setAccessible(true);\n            Object loadedApk = loadedApkField.get(app);\n\n            Field activityThreadField = loadedApk.getClass().getDeclaredField(\"mActivityThread\");\n            activityThreadField.setAccessible(true);\n            Object activityThread = activityThreadField.get(loadedApk);\n\n            Method getProcessName = activityThread.getClass().getDeclaredMethod(\"getProcessName\");\n            processName = (String) getProcessName.invoke(activityThread);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return processName;\n    }\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/Provider.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\n/**\n * @author cenxiaozhong\n * @date 2017/7/5\n * @since 1.0.0\n */\npublic interface Provider<T> {\n   T provide();\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/QuickCallJs.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.os.Build;\nimport android.webkit.ValueCallback;\n\nimport androidx.annotation.RequiresApi;\n\n\n/**\n * @author cenxiaozhong\n * @date 2017/5/29\n * @since 1.0.0\n */\npublic interface QuickCallJs {\n    @RequiresApi(Build.VERSION_CODES.KITKAT)\n    void quickCallJs(String method, ValueCallback<String> callback, String... params);\n\n    void quickCallJs(String method, String... params);\n\n    void quickCallJs(String method);\n\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/UrlCommonException.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\npublic class UrlCommonException extends RuntimeException {\n\n    public UrlCommonException() {\n    }\n\n    public UrlCommonException(String msg) {\n        super(msg);\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/UrlLoaderImpl.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.webkit.WebView;\n\nimport java.util.Map;\n\n/**\n * @author cenxiaozhong\n * @since 2.0.0\n */\npublic class UrlLoaderImpl implements IUrlLoader {\n\tprivate Handler mHandler = null;\n\tprivate WebView mWebView;\n\tprivate HttpHeaders mHttpHeaders;\n\tpublic static final String TAG = UrlLoaderImpl.class.getSimpleName();\n\n\tUrlLoaderImpl(WebView webView, HttpHeaders httpHeaders) {\n\t\tthis.mWebView = webView;\n\t\tif (this.mWebView == null) {\n\t\t\tnew NullPointerException(\"webview cannot be null .\");\n\t\t}\n\t\tthis.mHttpHeaders = httpHeaders;\n\t\tif (this.mHttpHeaders == null) {\n\t\t\tthis.mHttpHeaders = HttpHeaders.create();\n\t\t}\n\t\tmHandler = new Handler(Looper.getMainLooper());\n\t}\n\n\tprivate void safeLoadUrl(final String url) {\n\t\tmHandler.post(new Runnable() {\n\t\t\t@Override\n\t\t\tpublic void run() {\n\t\t\t\tloadUrl(url);\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate void safeReload() {\n\t\tmHandler.post(new Runnable() {\n\t\t\t@Override\n\t\t\tpublic void run() {\n\t\t\t\treload();\n\t\t\t}\n\t\t});\n\t}\n\n\t@Override\n\tpublic void loadUrl(String url) {\n\t\tthis.loadUrl(url, this.mHttpHeaders.getHeaders(url));\n\t}\n\n\t@Override\n\tpublic void loadUrl(final String url, final Map<String, String> headers) {\n\t\tif (!AgentWebUtils.isUIThread()) {\n\t\t\tAgentWebUtils.runInUiThread(new Runnable() {\n\t\t\t\t@Override\n\t\t\t\tpublic void run() {\n\t\t\t\t\tloadUrl(url, headers);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\tLogUtils.i(TAG, \"loadUrl:\" + url + \" headers:\" + headers);\n\t\tif (headers == null || headers.isEmpty()) {\n\t\t\tthis.mWebView.loadUrl(url);\n\t\t} else {\n\t\t\tthis.mWebView.loadUrl(url, headers);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void reload() {\n\t\tif (!AgentWebUtils.isUIThread()) {\n\t\t\tmHandler.post(new Runnable() {\n\t\t\t\t@Override\n\t\t\t\tpublic void run() {\n\t\t\t\t\treload();\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tthis.mWebView.reload();\n\t}\n\n\t@Override\n\tpublic void loadData(final String data, final String mimeType, final String encoding) {\n\t\tif (!AgentWebUtils.isUIThread()) {\n\t\t\tmHandler.post(new Runnable() {\n\t\t\t\t@Override\n\t\t\t\tpublic void run() {\n\t\t\t\t\tloadData(data, mimeType, encoding);\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tthis.mWebView.loadData(data, mimeType, encoding);\n\t}\n\n\t@Override\n\tpublic void stopLoading() {\n\t\tif (!AgentWebUtils.isUIThread()) {\n\t\t\tmHandler.post(new Runnable() {\n\t\t\t\t@Override\n\t\t\t\tpublic void run() {\n\t\t\t\t\tstopLoading();\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tthis.mWebView.stopLoading();\n\t}\n\n\t@Override\n\tpublic void loadDataWithBaseURL(final String baseUrl, final String data, final String mimeType, final String encoding, final String historyUrl) {\n\t\tif (!AgentWebUtils.isUIThread()) {\n\t\t\tmHandler.post(new Runnable() {\n\t\t\t\t@Override\n\t\t\t\tpublic void run() {\n\t\t\t\t\tloadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tthis.mWebView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);\n\t}\n\n\t@Override\n\tpublic void postUrl(final String url, final byte[] postData) {\n\t\tif (!AgentWebUtils.isUIThread()) {\n\t\t\tmHandler.post(new Runnable() {\n\t\t\t\t@Override\n\t\t\t\tpublic void run() {\n\t\t\t\t\tpostUrl(url, postData);\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tthis.mWebView.postUrl(url, postData);\n\t}\n\n\t@Override\n\tpublic HttpHeaders getHttpHeaders() {\n\t\treturn this.mHttpHeaders == null ? this.mHttpHeaders = HttpHeaders.create() : this.mHttpHeaders;\n\t}\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/VideoImpl.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.app.Activity;\nimport android.content.pm.ActivityInfo;\nimport android.graphics.Color;\nimport android.os.Build;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.Window;\nimport android.view.WindowManager;\nimport android.webkit.WebChromeClient;\nimport android.webkit.WebView;\nimport android.widget.FrameLayout;\n\nimport androidx.core.util.Pair;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\n/**\n * @author cenxiaozhong\n */\npublic class VideoImpl implements IVideo, EventInterceptor {\n\n    private Activity mActivity;\n    private WebView mWebView;\n    private static final String TAG = VideoImpl.class.getSimpleName();\n    private Set<Pair<Integer, Integer>> mFlags = null;\n    private View mMoiveView = null;\n    private ViewGroup mMoiveParentView = null;\n    private WebChromeClient.CustomViewCallback mCallback;\n\n    public VideoImpl(Activity mActivity, WebView webView) {\n        this.mActivity = mActivity;\n        this.mWebView = webView;\n        mFlags = new HashSet<>();\n    }\n\n    @Override\n    public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) {\n        Activity mActivity;\n        if ((mActivity = this.mActivity) == null || mActivity.isFinishing()) {\n            return;\n        }\n        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);\n        Window mWindow = mActivity.getWindow();\n        Pair<Integer, Integer> mPair = null;\n        // 保存当前屏幕的状态\n        if ((mWindow.getAttributes().flags & WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) == 0) {\n            mPair = new Pair<>(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, 0);\n            mWindow.setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);\n            mFlags.add(mPair);\n        }\n        if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) && (mWindow.getAttributes().flags & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) == 0) {\n            mPair = new Pair<>(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, 0);\n            mWindow.setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);\n            mFlags.add(mPair);\n        }\n        if (mMoiveView != null) {\n            callback.onCustomViewHidden();\n            return;\n        }\n        if (mWebView != null) {\n            mWebView.setVisibility(View.GONE);\n        }\n        if (mMoiveParentView == null) {\n            FrameLayout mDecorView = (FrameLayout) mActivity.getWindow().getDecorView();\n            mMoiveParentView = new FrameLayout(mActivity);\n            mMoiveParentView.setBackgroundColor(Color.BLACK);\n            mDecorView.addView(mMoiveParentView);\n        }\n        this.mCallback = callback;\n        mMoiveParentView.addView(this.mMoiveView = view);\n        mMoiveParentView.setVisibility(View.VISIBLE);\n    }\n\n    @Override\n    public void onHideCustomView() {\n        if (mMoiveView == null) {\n            return;\n        }\n        if (mActivity != null && mActivity.getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {\n            mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);\n        }\n        if (!mFlags.isEmpty()) {\n            for (Pair<Integer, Integer> mPair : mFlags) {\n                mActivity.getWindow().setFlags(mPair.second, mPair.first);\n            }\n            mFlags.clear();\n        }\n        mMoiveView.setVisibility(View.GONE);\n        if (mMoiveParentView != null && mMoiveView != null) {\n            mMoiveParentView.removeView(mMoiveView);\n\n        }\n        if (mMoiveParentView != null) {\n            mMoiveParentView.setVisibility(View.GONE);\n        }\n        if (this.mCallback != null) {\n            mCallback.onCustomViewHidden();\n        }\n        this.mMoiveView = null;\n        if (mWebView != null) {\n            mWebView.setVisibility(View.VISIBLE);\n        }\n    }\n\n    @Override\n    public boolean isVideoState() {\n        return mMoiveView != null;\n    }\n\n    @Override\n    public boolean event() {\n\n        if (isVideoState()) {\n            onHideCustomView();\n            return true;\n        } else {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/WebChromeClient.java",
    "content": "package com.just.agentweb;\n\n/**\n * @author cenxiaozhong\n * @date 2019/4/13\n * @since 1.0.0\n */\npublic class WebChromeClient extends MiddlewareWebChromeBase{\n\tpublic WebChromeClient() {\n\t}\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/WebChromeClientDelegate.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.graphics.Bitmap;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Message;\nimport android.view.View;\nimport android.webkit.ConsoleMessage;\nimport android.webkit.GeolocationPermissions;\nimport android.webkit.JsPromptResult;\nimport android.webkit.JsResult;\nimport android.webkit.PermissionRequest;\nimport android.webkit.ValueCallback;\nimport android.webkit.WebChromeClient;\nimport android.webkit.WebStorage;\nimport android.webkit.WebView;\n\nimport androidx.annotation.RequiresApi;\n\nimport java.lang.reflect.Method;\n\n/**\n * @update WebChromeClientWrapper rename to WebChromeClientDelegate\n * @author cenxiaozhong\n * @since 1.0.0\n */\npublic class WebChromeClientDelegate extends WebChromeClient {\n    private WebChromeClient mDelegate;\n\n    protected WebChromeClient getDelegate() {\n        return mDelegate;\n    }\n\n    public WebChromeClientDelegate(WebChromeClient webChromeClient) {\n        this.mDelegate = webChromeClient;\n    }\n\n    void setDelegate(WebChromeClient delegate) {\n        this.mDelegate = delegate;\n    }\n\n    @Override\n    public void onProgressChanged(WebView view, int newProgress) {\n        super.onProgressChanged(view, newProgress);\n        if (this.mDelegate != null) {\n            this.mDelegate.onProgressChanged(view, newProgress);\n            return;\n        }\n    }\n\n    @Override\n    public void onReceivedTitle(WebView view, String title) {\n        if (this.mDelegate != null) {\n            this.mDelegate.onReceivedTitle(view, title);\n            return;\n        }\n        super.onReceivedTitle(view, title);\n    }\n\n    @Override\n    public void onReceivedIcon(WebView view, Bitmap icon) {\n        if (this.mDelegate != null) {\n            this.mDelegate.onReceivedIcon(view, icon);\n            return;\n        }\n        super.onReceivedIcon(view, icon);\n    }\n\n    @Override\n    public void onReceivedTouchIconUrl(WebView view, String url,\n                                       boolean precomposed) {\n        if (this.mDelegate != null) {\n            this.mDelegate.onReceivedTouchIconUrl(view, url, precomposed);\n            return;\n        }\n        super.onReceivedTouchIconUrl(view, url, precomposed);\n    }\n\n    @Override\n    public void onShowCustomView(View view, CustomViewCallback callback) {\n        if (this.mDelegate != null) {\n            this.mDelegate.onShowCustomView(view, callback);\n            return;\n        }\n        super.onShowCustomView(view, callback);\n    }\n\n\n    @Override\n    public void onShowCustomView(View view, int requestedOrientation,\n                                 CustomViewCallback callback) {\n        if (this.mDelegate != null) {\n            this.mDelegate.onShowCustomView(view, requestedOrientation, callback);\n            return;\n        }\n        super.onShowCustomView(view, requestedOrientation, callback);\n    }\n\n\n    @Override\n    public void onHideCustomView() {\n        if (this.mDelegate != null) {\n            this.mDelegate.onHideCustomView();\n            return;\n        }\n        super.onHideCustomView();\n    }\n\n    @Override\n    public boolean onCreateWindow(WebView view, boolean isDialog,\n                                  boolean isUserGesture, Message resultMsg) {\n        if (this.mDelegate != null) {\n            return this.mDelegate.onCreateWindow(view, isDialog, isUserGesture, resultMsg);\n        }\n        return super.onCreateWindow(view, isDialog, isUserGesture, resultMsg);\n    }\n\n    @Override\n    public void onRequestFocus(WebView view) {\n        if (this.mDelegate != null) {\n            this.mDelegate.onRequestFocus(view);\n            return;\n        }\n        super.onRequestFocus(view);\n    }\n\n    @Override\n    public void onCloseWindow(WebView window) {\n        if (this.mDelegate != null) {\n            this.mDelegate.onCloseWindow(window);\n            return;\n        }\n        super.onCloseWindow(window);\n    }\n\n    @Override\n    public boolean onJsAlert(WebView view, String url, String message,\n                             JsResult result) {\n        if (this.mDelegate != null) {\n            return this.mDelegate.onJsAlert(view, url, message, result);\n        }\n        return super.onJsAlert(view, url, message, result);\n    }\n\n    @Override\n    public boolean onJsConfirm(WebView view, String url, String message,\n                               JsResult result) {\n        if (this.mDelegate != null) {\n            return this.mDelegate.onJsConfirm(view, url, message, result);\n        }\n        return super.onJsConfirm(view, url, message, result);\n    }\n\n    @Override\n    public boolean onJsPrompt(WebView view, String url, String message,\n                              String defaultValue, JsPromptResult result) {\n        if (this.mDelegate != null) {\n            return this.mDelegate.onJsPrompt(view, url, message, defaultValue, result);\n        }\n        return super.onJsPrompt(view, url, message, defaultValue, result);\n    }\n\n    @Override\n    public boolean onJsBeforeUnload(WebView view, String url, String message,\n                                    JsResult result) {\n        if (this.mDelegate != null) {\n            return this.mDelegate.onJsBeforeUnload(view, url, message, result);\n        }\n        return super.onJsBeforeUnload(view, url, message, result);\n    }\n\n    @Override\n    @Deprecated\n    public void onExceededDatabaseQuota(String url, String databaseIdentifier,\n                                        long quota, long estimatedDatabaseSize, long totalQuota,\n                                        WebStorage.QuotaUpdater quotaUpdater) {\n        // This default implementation passes the current quota back to WebCore.\n        // WebCore will interpret this that new quota was declined.\n        if (this.mDelegate != null) {\n            this.mDelegate.onExceededDatabaseQuota(url, databaseIdentifier, quota, estimatedDatabaseSize, totalQuota, quotaUpdater);\n            return;\n        }\n        super.onExceededDatabaseQuota(url, databaseIdentifier, quota, estimatedDatabaseSize, totalQuota, quotaUpdater);\n\n    }\n\n    @Override\n    @Deprecated\n    public void onReachedMaxAppCacheSize(long requiredStorage, long quota,\n                                         WebStorage.QuotaUpdater quotaUpdater) {\n        if (this.mDelegate != null) {\n            this.mDelegate.onReachedMaxAppCacheSize(requiredStorage, quota, quotaUpdater);\n            return;\n        }\n        super.onReachedMaxAppCacheSize(requiredStorage, quota, quotaUpdater);\n    }\n\n    @Override\n    public void onGeolocationPermissionsShowPrompt(String origin,\n                                                   GeolocationPermissions.Callback callback) {\n        if (this.mDelegate != null) {\n            this.mDelegate.onGeolocationPermissionsShowPrompt(origin, callback);\n            return;\n        }\n        super.onGeolocationPermissionsShowPrompt(origin, callback);\n\n    }\n\n    /**\n     * notify the host application that a request for Geolocation permissions,\n     * made with a previous call to\n     * {@link #onGeolocationPermissionsShowPrompt(String, GeolocationPermissions.Callback) onGeolocationPermissionsShowPrompt()}\n     * has been canceled. Any related UI should therefore be hidden.\n     */\n    @Override\n    public void onGeolocationPermissionsHidePrompt() {\n\n        if (this.mDelegate != null) {\n            this.mDelegate.onGeolocationPermissionsHidePrompt();\n            return;\n        }\n\n        super.onGeolocationPermissionsHidePrompt();\n    }\n\n    @Override\n    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n    public void onPermissionRequest(PermissionRequest request) {\n        if (this.mDelegate != null) {\n            this.mDelegate.onPermissionRequest(request);\n            return;\n        }\n        super.onPermissionRequest(request);\n    }\n\n    @Override\n    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n    public void onPermissionRequestCanceled(PermissionRequest request) {\n\n        if (this.mDelegate != null) {\n            this.mDelegate.onPermissionRequestCanceled(request);\n            return;\n        }\n        super.onPermissionRequestCanceled(request);\n    }\n\n    @Override\n    public boolean onJsTimeout() {\n        if (this.mDelegate != null) {\n            return this.mDelegate.onJsTimeout();\n        }\n        return super.onJsTimeout();\n    }\n\n    @Override\n    @Deprecated\n    public void onConsoleMessage(String message, int lineNumber, String sourceID) {\n        if (this.mDelegate != null) {\n            this.mDelegate.onConsoleMessage(message, lineNumber, sourceID);\n            return;\n        }\n        super.onConsoleMessage(message, lineNumber, sourceID);\n    }\n\n    @Override\n    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {\n        /*onConsoleMessage(consoleMessage.message(), consoleMessage.lineNumber(),\n                consoleMessage.sourceId());*/\n\n        if (this.mDelegate != null) {\n            return this.mDelegate.onConsoleMessage(consoleMessage);\n        }\n        return super.onConsoleMessage(consoleMessage);\n    }\n\n    @Override\n    public Bitmap getDefaultVideoPoster() {\n        if (this.mDelegate != null) {\n            return this.mDelegate.getDefaultVideoPoster();\n        }\n        return super.getDefaultVideoPoster();\n    }\n\n    @Override\n    public View getVideoLoadingProgressView() {\n        if (this.mDelegate != null) {\n            return this.mDelegate.getVideoLoadingProgressView();\n        }\n        return super.getVideoLoadingProgressView();\n    }\n\n    @Override\n    public void getVisitedHistory(ValueCallback<String[]> callback) {\n        if (this.mDelegate != null) {\n            this.mDelegate.getVisitedHistory(callback);\n            return;\n        }\n        super.getVisitedHistory(callback);\n    }\n\n    @Override\n    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n    public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,\n                                     FileChooserParams fileChooserParams) {\n        if (this.mDelegate != null) {\n            return this.mDelegate.onShowFileChooser(webView, filePathCallback, fileChooserParams);\n        }\n        return super.onShowFileChooser(webView, filePathCallback, fileChooserParams);\n    }\n\n\n    /**\n     * Android  >= 4.1\n     * @param uploadFile\n     * @param acceptType\n     * @param capture\n     */\n    public void openFileChooser(ValueCallback<Uri> uploadFile, String acceptType, String capture) {\n        commonRefect(this.mDelegate, \"openFileChooser\", new Object[]{uploadFile, acceptType, capture}, ValueCallback.class, String.class, String.class);\n    }\n\n    /**\n     * Android < 3.0\n     * @param valueCallback\n     */\n    public void openFileChooser(ValueCallback<Uri> valueCallback) {\n        commonRefect(this.mDelegate, \"openFileChooser\", new Object[]{valueCallback}, ValueCallback.class);\n    }\n\n    /**\n     * Android  >= 3.0\n     * @param valueCallback\n     * @param acceptType\n     */\n    public void openFileChooser(ValueCallback valueCallback, String acceptType) {\n        commonRefect(this.mDelegate, \"openFileChooser\", new Object[]{valueCallback, acceptType}, ValueCallback.class, String.class);\n    }\n\n\n    private void commonRefect(WebChromeClient o, String mothed, Object[] os, Class... clazzs) {\n        try {\n            if (o == null) {\n                return;\n            }\n            Class<?> clazz = o.getClass();\n            Method mMethod = clazz.getMethod(mothed, clazzs);\n            mMethod.invoke(o, os);\n        } catch (Exception ignore) {\n            if (LogUtils.isDebug()) {\n                ignore.printStackTrace();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/WebCreator.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.webkit.WebView;\nimport android.widget.FrameLayout;\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\npublic interface WebCreator extends IWebIndicator {\n    WebCreator create();\n\n    WebView getWebView();\n\n    FrameLayout getWebParentLayout();\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/WebIndicator.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.AnimatorSet;\nimport android.animation.ObjectAnimator;\nimport android.animation.ValueAnimator;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.view.animation.DecelerateInterpolator;\nimport android.view.animation.LinearInterpolator;\n\nimport androidx.annotation.Nullable;\n\n/**\n * @author cenxiaozhong\n * @since 1.0.0\n */\npublic class WebIndicator extends BaseIndicatorView implements BaseIndicatorSpec {\n    /**\n     * 进度条颜色\n     */\n    private int mColor;\n    /**\n     * 进度条的画笔\n     */\n    private Paint mPaint;\n    /**\n     * 进度条动画\n     */\n    private Animator mAnimator;\n    /**\n     * 控件的宽度\n     */\n    private int mTargetWidth = 0;\n    /**\n     * 默认匀速动画最大的时长\n     */\n    public static final int MAX_UNIFORM_SPEED_DURATION = 8 * 1000;\n    /**\n     * 默认加速后减速动画最大时长\n     */\n    public static final int MAX_DECELERATE_SPEED_DURATION = 450;\n    /**\n     * 结束动画时长 ， Fade out 。\n     */\n    public static final int DO_END_ANIMATION_DURATION = 600;\n\n    /**\n     * 当前匀速动画最大的时长\n     */\n    private static int CURRENT_MAX_UNIFORM_SPEED_DURATION = MAX_UNIFORM_SPEED_DURATION;\n    /**\n     * 当前加速后减速动画最大时长\n     */\n    private static int CURRENT_MAX_DECELERATE_SPEED_DURATION = MAX_DECELERATE_SPEED_DURATION;\n\n    /**\n     * 标志当前进度条的状态\n     */\n    private int TAG = 0;\n    public static final int UN_START = 0;\n    public static final int STARTED = 1;\n    public static final int FINISH = 2;\n    private float mTarget = 0f;\n    private float mCurrentProgress = 0F;\n    /**\n     * 默认的高度\n     */\n    public static int WEB_INDICATOR_DEFAULT_HEIGHT = 3;\n\n    public WebIndicator(Context context) {\n        this(context, null);\n    }\n\n    public WebIndicator(Context context, @Nullable AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public WebIndicator(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs, defStyleAttr);\n    }\n\n    private void init(Context context, AttributeSet attrs, int defStyleAttr) {\n        mPaint = new Paint();\n        mColor = Color.parseColor(\"#1aad19\");\n        mPaint.setAntiAlias(true);\n        mPaint.setColor(mColor);\n        mPaint.setDither(true);\n        mPaint.setStrokeCap(Paint.Cap.SQUARE);\n        mTargetWidth = context.getResources().getDisplayMetrics().widthPixels;\n        WEB_INDICATOR_DEFAULT_HEIGHT = AgentWebUtils.dp2px(context, 3);\n    }\n\n    public void setColor(int color) {\n        this.mColor = color;\n        mPaint.setColor(color);\n    }\n\n    public void setColor(String color) {\n        this.setColor(Color.parseColor(color));\n    }\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        int wMode = MeasureSpec.getMode(widthMeasureSpec);\n        int w = MeasureSpec.getSize(widthMeasureSpec);\n        int hMode = MeasureSpec.getMode(heightMeasureSpec);\n        int h = MeasureSpec.getSize(heightMeasureSpec);\n\n        if (wMode == MeasureSpec.AT_MOST) {\n            w = w <= getContext().getResources().getDisplayMetrics().widthPixels ? w : getContext().getResources().getDisplayMetrics().widthPixels;\n        }\n        if (hMode == MeasureSpec.AT_MOST) {\n            h = WEB_INDICATOR_DEFAULT_HEIGHT;\n        }\n        this.setMeasuredDimension(w, h);\n    }\n\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n    }\n\n    @Override\n    protected void dispatchDraw(Canvas canvas) {\n        canvas.drawRect(0, 0, mCurrentProgress / 100 * Float.valueOf(this.getWidth()), this.getHeight(), mPaint);\n    }\n    @Override\n    public void show() {\n        if (getVisibility() == View.GONE) {\n            this.setVisibility(View.VISIBLE);\n            mCurrentProgress = 0f;\n            startAnim(false);\n        }\n    }\n\n    @Override\n    protected void onSizeChanged(int w, int h, int oldw, int oldh) {\n        super.onSizeChanged(w, h, oldw, oldh);\n        this.mTargetWidth = getMeasuredWidth();\n        int screenWidth = getContext().getResources().getDisplayMetrics().widthPixels;\n        if (mTargetWidth >= screenWidth) {\n            CURRENT_MAX_DECELERATE_SPEED_DURATION = MAX_DECELERATE_SPEED_DURATION;\n            CURRENT_MAX_UNIFORM_SPEED_DURATION = MAX_UNIFORM_SPEED_DURATION;\n        } else {\n            //取比值\n            float rate = this.mTargetWidth / Float.valueOf(screenWidth);\n            CURRENT_MAX_UNIFORM_SPEED_DURATION = (int) (MAX_UNIFORM_SPEED_DURATION * rate);\n            CURRENT_MAX_DECELERATE_SPEED_DURATION = (int) (MAX_DECELERATE_SPEED_DURATION * rate);\n        }\n        LogUtils.i(\"WebProgress\", \"CURRENT_MAX_UNIFORM_SPEED_DURATION\" + CURRENT_MAX_UNIFORM_SPEED_DURATION);\n    }\n\n    public void setProgress(float progress) {\n        if (getVisibility() == View.GONE) {\n            setVisibility(View.VISIBLE);\n        }\n        if (progress < 95f){\n            return;\n        }\n        if (TAG != FINISH) {\n            startAnim(true);\n        }\n    }\n    @Override\n    public void hide() {\n        TAG = FINISH;\n    }\n\n    private void startAnim(boolean isFinished) {\n        float v = isFinished ? 100 : 95;\n        if (mAnimator != null && mAnimator.isStarted()) {\n            mAnimator.cancel();\n        }\n        mCurrentProgress = mCurrentProgress == 0f ? 0.00000001f : mCurrentProgress;\n        LogUtils.i(\"WebIndicator\", \"mCurrentProgress:\" + mCurrentProgress + \" v:\" + v + \"  :\" + (1f - mCurrentProgress));\n        if (!isFinished) {\n            ValueAnimator mAnimator = ValueAnimator.ofFloat(mCurrentProgress, v);\n            float residue = 1f - mCurrentProgress / 100 - 0.05f;\n            mAnimator.setInterpolator(new LinearInterpolator());\n            mAnimator.setDuration((long) (residue * CURRENT_MAX_UNIFORM_SPEED_DURATION));\n            mAnimator.addUpdateListener(mAnimatorUpdateListener);\n            mAnimator.start();\n            this.mAnimator = mAnimator;\n        } else {\n            ValueAnimator segment95Animator = null;\n            if (mCurrentProgress < 95f) {\n                segment95Animator = ValueAnimator.ofFloat(mCurrentProgress, 95);\n                float residue = 1f - mCurrentProgress / 100f - 0.05f;\n                segment95Animator.setInterpolator(new LinearInterpolator());\n                segment95Animator.setDuration((long) (residue * CURRENT_MAX_DECELERATE_SPEED_DURATION));\n                segment95Animator.setInterpolator(new DecelerateInterpolator());\n                segment95Animator.addUpdateListener(mAnimatorUpdateListener);\n            }\n            ObjectAnimator mObjectAnimator = ObjectAnimator.ofFloat(this, \"alpha\", 1f, 0f);\n            mObjectAnimator.setDuration(DO_END_ANIMATION_DURATION);\n            ValueAnimator mValueAnimatorEnd = ValueAnimator.ofFloat(95f, 100f);\n            mValueAnimatorEnd.setDuration(DO_END_ANIMATION_DURATION);\n            mValueAnimatorEnd.addUpdateListener(mAnimatorUpdateListener);\n            AnimatorSet mAnimatorSet = new AnimatorSet();\n            mAnimatorSet.playTogether(mObjectAnimator, mValueAnimatorEnd);\n            if (segment95Animator != null) {\n                AnimatorSet mAnimatorSet1 = new AnimatorSet();\n                mAnimatorSet1.play(mAnimatorSet).after(segment95Animator);\n                mAnimatorSet = mAnimatorSet1;\n            }\n            mAnimatorSet.addListener(mAnimatorListenerAdapter);\n            mAnimatorSet.start();\n            mAnimator = mAnimatorSet;\n        }\n        TAG = STARTED;\n        mTarget = v;\n    }\n\n    private ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {\n        @Override\n        public void onAnimationUpdate(ValueAnimator animation) {\n            float t = (float) animation.getAnimatedValue();\n            WebIndicator.this.mCurrentProgress = t;\n            WebIndicator.this.invalidate();\n        }\n    };\n\n    private AnimatorListenerAdapter mAnimatorListenerAdapter = new AnimatorListenerAdapter() {\n        @Override\n        public void onAnimationEnd(Animator animation) {\n            doEnd();\n        }\n    };\n\n    @Override\n    protected void onDetachedFromWindow() {\n        super.onDetachedFromWindow();\n        /**\n         * animator cause leak , if not cancel;\n         */\n        if (mAnimator != null && mAnimator.isStarted()) {\n            mAnimator.cancel();\n            mAnimator = null;\n        }\n    }\n\n    private void doEnd() {\n        if (TAG == FINISH && mCurrentProgress == 100f) {\n            setVisibility(GONE);\n            mCurrentProgress = 0f;\n            this.setAlpha(1f);\n        }\n        TAG = UN_START;\n    }\n\n    @Override\n    public void reset() {\n        mCurrentProgress = 0;\n        if (mAnimator != null && mAnimator.isStarted()){\n            mAnimator.cancel();\n        }\n    }\n\n    @Override\n    public void setProgress(int newProgress) {\n        setProgress(Float.valueOf(newProgress));\n    }\n\n\n    @Override\n    public LayoutParams offerLayoutParams() {\n        return new LayoutParams(-1, WEB_INDICATOR_DEFAULT_HEIGHT);\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/WebLifeCycle.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\n/**\n * @author cenxiaozhong\n * @date 2017/5/30\n * @since 1.0.0\n */\npublic interface WebLifeCycle {\n    void onResume();\n    void onPause();\n    void onDestroy();\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/WebListenerManager.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.webkit.DownloadListener;\nimport android.webkit.WebChromeClient;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\n\n/**\n * @author cenxiaozhong\n * @date 2017/5/13\n * @since 1.0.0\n */\npublic interface WebListenerManager {\n    WebListenerManager setWebChromeClient(WebView webview, WebChromeClient webChromeClient);\n    WebListenerManager setWebViewClient(WebView webView, WebViewClient webViewClient);\n    WebListenerManager setDownloader(WebView webView, DownloadListener downloadListener);\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/WebParentLayout.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.util.AttributeSet;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.ViewStub;\nimport android.webkit.WebView;\nimport android.widget.FrameLayout;\n\nimport androidx.annotation.IdRes;\nimport androidx.annotation.LayoutRes;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\n/**\n * @author cenxiaozhong\n * @date 2017/12/8\n * @since 3.0.0\n */\npublic class WebParentLayout extends FrameLayout implements Provider<AbsAgentWebUIController> {\n\tprivate AbsAgentWebUIController mAgentWebUIController = null;\n\tprivate static final String TAG = WebParentLayout.class.getSimpleName();\n\t@LayoutRes\n\tprivate int mErrorLayoutRes;\n\t@IdRes\n\tprivate int mClickId = -1;\n\tprivate View mErrorView;\n\tprivate WebView mWebView;\n\tprivate FrameLayout mErrorLayout = null;\n\n\tWebParentLayout(@NonNull Context context) {\n\t\tthis(context, null);\n\t\tLogUtils.i(TAG, \"WebParentLayout\");\n\t}\n\n\tWebParentLayout(@NonNull Context context, @Nullable AttributeSet attrs) {\n\t\tthis(context, attrs, -1);\n\t}\n\n\tWebParentLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n\t\tsuper(context, attrs, defStyleAttr);\n\t\tif (!(context instanceof Activity)) {\n\t\t\tthrow new IllegalArgumentException(\"WebParentLayout context must be activity or activity sub class .\");\n\t\t}\n\t\tthis.mErrorLayoutRes = R.layout.agentweb_error_page;\n\t}\n\n\tvoid bindController(AbsAgentWebUIController agentWebUIController) {\n\t\tthis.mAgentWebUIController = agentWebUIController;\n\t\tthis.mAgentWebUIController.bindWebParent(this, (Activity) getContext());\n\t}\n\n\tvoid showPageMainFrameError() {\n\t\tView container = this.mErrorLayout;\n\t\tif (container != null) {\n\t\t\tcontainer.setVisibility(View.VISIBLE);\n\t\t} else {\n\t\t\tcreateErrorLayout();\n\t\t\tcontainer = this.mErrorLayout;\n\t\t}\n\t\tView clickView = null;\n\t\tif (mClickId != -1 && (clickView = container.findViewById(mClickId)) != null) {\n\t\t\tclickView.setClickable(true);\n\t\t} else {\n\t\t\tcontainer.setClickable(true);\n\t\t}\n\t}\n\n\tprivate void createErrorLayout() {\n\t\tfinal FrameLayout mFrameLayout = new FrameLayout(getContext());\n\t\tmFrameLayout.setBackgroundColor(Color.WHITE);\n\t\tmFrameLayout.setId(R.id.mainframe_error_container_id);\n\t\tif (this.mErrorView == null) {\n\t\t\tLayoutInflater mLayoutInflater = LayoutInflater.from(getContext());\n\t\t\tLogUtils.i(TAG, \"mErrorLayoutRes:\" + mErrorLayoutRes);\n\t\t\tmLayoutInflater.inflate(mErrorLayoutRes, mFrameLayout, true);\n\t\t} else {\n\t\t\tmFrameLayout.addView(mErrorView);\n\t\t}\n\t\tViewStub mViewStub = (ViewStub) this.findViewById(R.id.mainframe_error_viewsub_id);\n\t\tfinal int index = this.indexOfChild(mViewStub);\n\t\tthis.removeViewInLayout(mViewStub);\n\t\tfinal ViewGroup.LayoutParams layoutParams = getLayoutParams();\n\t\tif (layoutParams != null) {\n\t\t\tthis.addView(this.mErrorLayout = mFrameLayout, index, layoutParams);\n\t\t} else {\n\t\t\tthis.addView(this.mErrorLayout = mFrameLayout, index);\n\t\t}\n\t\tmFrameLayout.setVisibility(View.VISIBLE);\n\t\tif (mClickId != -1) {\n\t\t\tfinal View clickView = mFrameLayout.findViewById(mClickId);\n\t\t\tif (clickView != null) {\n\t\t\t\tclickView.setOnClickListener(new OnClickListener() {\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic void onClick(View v) {\n\t\t\t\t\t\tif (getWebView() != null) {\n\t\t\t\t\t\t\tclickView.setClickable(false);\n\t\t\t\t\t\t\tgetWebView().reload();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tif (LogUtils.isDebug()) {\n\t\t\t\t\tLogUtils.e(TAG, \"ClickView is null , cannot bind accurate view to refresh or reload .\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tmFrameLayout.setOnClickListener(new OnClickListener() {\n\t\t\t@Override\n\t\t\tpublic void onClick(View v) {\n\t\t\t\tif (getWebView() != null) {\n\t\t\t\t\tmFrameLayout.setClickable(false);\n\t\t\t\t\tgetWebView().reload();\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n\tvoid hideErrorLayout() {\n\t\tView mView = null;\n\t\tif ((mView = this.findViewById(R.id.mainframe_error_container_id)) != null) {\n\t\t\tmView.setVisibility(View.GONE);\n\t\t}\n\t}\n\n\tvoid setErrorView(@NonNull View errorView) {\n\t\tthis.mErrorView = errorView;\n\t}\n\n\tvoid setErrorLayoutRes(@LayoutRes int resLayout, @IdRes int id) {\n\t\tthis.mClickId = id;\n\t\tif (this.mClickId <= 0) {\n\t\t\tthis.mClickId = -1;\n\t\t}\n\t\tthis.mErrorLayoutRes = resLayout;\n\t\tif (this.mErrorLayoutRes <= 0) {\n\t\t\tthis.mErrorLayoutRes = R.layout.agentweb_error_page;\n\t\t}\n\t}\n\n\t@Override\n\tpublic AbsAgentWebUIController provide() {\n\t\treturn this.mAgentWebUIController;\n\t}\n\n\n\tvoid bindWebView(WebView view) {\n\t\tif (this.mWebView == null) {\n\t\t\tthis.mWebView = view;\n\t\t}\n\t}\n\n\tWebView getWebView() {\n\t\treturn this.mWebView;\n\t}\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/WebSecurityCheckLogic.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.webkit.WebView;\n\nimport androidx.collection.ArrayMap;\n\n\n/**\n * @author cenxiaozhong\n */\npublic interface WebSecurityCheckLogic {\n    void dealHoneyComb(WebView view);\n    void dealJsInterface(ArrayMap<String, Object> objects, AgentWeb.SecurityType securityType);\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/WebSecurityController.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\n/**\n * @author cenxiaozhong\n */\npublic interface WebSecurityController<T> {\n    void check(T t);\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/WebSecurityControllerImpl.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.os.Build;\nimport android.webkit.WebView;\n\nimport androidx.collection.ArrayMap;\n\n\n/**\n * @author cenxiaozhong\n */\npublic class WebSecurityControllerImpl implements WebSecurityController<WebSecurityCheckLogic> {\n\n\tprivate WebView mWebView;\n\tprivate ArrayMap<String, Object> mMap;\n\tprivate AgentWeb.SecurityType mSecurityType;\n\n\tpublic WebSecurityControllerImpl(WebView view, ArrayMap<String, Object> map, AgentWeb.SecurityType securityType) {\n\t\tthis.mWebView = view;\n\t\tthis.mMap = map;\n\t\tthis.mSecurityType = securityType;\n\t}\n\n\t@Override\n\tpublic void check(WebSecurityCheckLogic webSecurityCheckLogic) {\n\t\tif (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) {\n\t\t\twebSecurityCheckLogic.dealHoneyComb(mWebView);\n\t\t}\n\t\tif (mMap != null && mSecurityType == AgentWeb.SecurityType.STRICT_CHECK && !mMap.isEmpty()) {\n\t\t\twebSecurityCheckLogic.dealJsInterface(mMap, mSecurityType);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/WebSecurityLogicImpl.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.annotation.TargetApi;\nimport android.os.Build;\nimport android.webkit.WebView;\n\nimport androidx.collection.ArrayMap;\n\n\n/**\n * @author cenxiaozhong\n */\npublic class WebSecurityLogicImpl implements WebSecurityCheckLogic {\n    private String TAG=this.getClass().getSimpleName();\n    public static WebSecurityLogicImpl getInstance() {\n        return new WebSecurityLogicImpl();\n    }\n\n    public WebSecurityLogicImpl(){}\n\n    @TargetApi(Build.VERSION_CODES.HONEYCOMB)\n    @Override\n    public void dealHoneyComb(WebView view) {\n        if (Build.VERSION_CODES.HONEYCOMB > Build.VERSION.SDK_INT || Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1){\n            return;\n        }\n        view.removeJavascriptInterface(\"searchBoxJavaBridge_\");\n        view.removeJavascriptInterface(\"accessibility\");\n        view.removeJavascriptInterface(\"accessibilityTraversal\");\n    }\n\n    @Override\n    public void dealJsInterface(ArrayMap<String, Object> objects, AgentWeb.SecurityType securityType) {\n        if (securityType== AgentWeb.SecurityType.STRICT_CHECK\n                &&AgentWebConfig.WEBVIEW_TYPE!=AgentWebConfig.WEBVIEW_AGENTWEB_SAFE_TYPE\n                &&Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {\n            LogUtils.e(TAG,\"Give up all inject objects\");\n            objects.clear();\n            objects = null;\n            System.gc();\n        }\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/WebViewClient.java",
    "content": "package com.just.agentweb;\n\n/**\n * @author cenxiaozhong\n * @date 2019/4/13\n * @since 1.0.0\n */\npublic class WebViewClient extends MiddlewareWebClientBase {\n\tpublic WebViewClient() {\n\t}\n\n}\n"
  },
  {
    "path": "agentweb-core/src/main/java/com/just/agentweb/WebViewClientDelegate.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.just.agentweb;\n\nimport android.graphics.Bitmap;\nimport android.net.http.SslError;\nimport android.os.Message;\nimport android.view.KeyEvent;\nimport android.webkit.ClientCertRequest;\nimport android.webkit.HttpAuthHandler;\nimport android.webkit.SslErrorHandler;\nimport android.webkit.WebResourceError;\nimport android.webkit.WebResourceRequest;\nimport android.webkit.WebResourceResponse;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\n\n/**\n * @update WrapperWebViewClient rename WebViewClientDelegate\n * @author cenxiaozhong\n * @date 2017/5/28\n */\npublic class WebViewClientDelegate extends WebViewClient {\n\n    private WebViewClient mDelegate;\n    private static final String TAG = WebViewClientDelegate.class.getSimpleName();\n\n    WebViewClientDelegate(WebViewClient client) {\n        this.mDelegate = client;\n    }\n\n    protected WebViewClient getDelegate() {\n        return mDelegate;\n    }\n\n    void setDelegate(WebViewClient delegate) {\n        this.mDelegate = delegate;\n    }\n\n    @Deprecated\n    @Override\n    public boolean shouldOverrideUrlLoading(WebView view, String url) {\n        if (mDelegate != null) {\n            return mDelegate.shouldOverrideUrlLoading(view, url);\n        }\n        return super.shouldOverrideUrlLoading(view, url);\n    }\n\n    @Override\n    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {\n        if (mDelegate != null) {\n            return mDelegate.shouldOverrideUrlLoading(view, request);\n        }\n        return super.shouldOverrideUrlLoading(view, request);\n    }\n\n    @Override\n    public void onPageStarted(WebView view, String url, Bitmap favicon) {\n        if (mDelegate != null) {\n            mDelegate.onPageStarted(view, url, favicon);\n            return;\n        }\n        super.onPageStarted(view, url, favicon);\n    }\n\n    @Override\n    public void onPageFinished(WebView view, String url) {\n        if (mDelegate != null) {\n            mDelegate.onPageFinished(view, url);\n            return;\n        }\n        super.onPageFinished(view, url);\n    }\n\n    @Override\n    public void onLoadResource(WebView view, String url) {\n        if (mDelegate != null) {\n            mDelegate.onLoadResource(view, url);\n            return;\n        }\n        super.onLoadResource(view, url);\n    }\n\n    @Override\n    public void onPageCommitVisible(WebView view, String url) {\n        if (mDelegate != null) {\n            mDelegate.onPageCommitVisible(view, url);\n            return;\n        }\n        super.onPageCommitVisible(view, url);\n    }\n\n    @Override\n    @Deprecated\n    public WebResourceResponse shouldInterceptRequest(WebView view,\n                                                      String url) {\n        if (mDelegate != null) {\n            return mDelegate.shouldInterceptRequest(view, url);\n        }\n        return super.shouldInterceptRequest(view, url);\n    }\n\n    @Override\n    public WebResourceResponse shouldInterceptRequest(WebView view,\n                                                      WebResourceRequest request) {\n        if (mDelegate != null) {\n            return mDelegate.shouldInterceptRequest(view, request);\n        }\n        return super.shouldInterceptRequest(view, request);\n    }\n\n    @Override\n    @Deprecated\n    public void onTooManyRedirects(WebView view, Message cancelMsg,\n                                   Message continueMsg) {\n        if (mDelegate != null) {\n            mDelegate.onTooManyRedirects(view, cancelMsg, continueMsg);\n            return;\n        }\n        super.onTooManyRedirects(view, cancelMsg, continueMsg);\n    }\n\n    @Override\n    @Deprecated\n    public void onReceivedError(WebView view, int errorCode,\n                                String description, String failingUrl) {\n        if (mDelegate != null) {\n            mDelegate.onReceivedError(view, errorCode, description, failingUrl);\n            return;\n        }\n        super.onReceivedError(view, errorCode, description, failingUrl);\n    }\n\n    @Override\n    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {\n        if (mDelegate != null) {\n            mDelegate.onReceivedError(view, request, error);\n            return;\n        }\n        super.onReceivedError(view, request, error);\n    }\n\n    @Override\n    public void onReceivedHttpError(\n            WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {\n        if (mDelegate != null) {\n            mDelegate.onReceivedHttpError(view, request, errorResponse);\n            return;\n        }\n        super.onReceivedHttpError(view, request, errorResponse);\n    }\n\n    @Override\n    public void onFormResubmission(WebView view, Message dontResend,\n                                   Message resend) {\n        if (mDelegate != null) {\n            mDelegate.onFormResubmission(view, dontResend, resend);\n            return;\n        }\n        super.onFormResubmission(view, dontResend, resend);\n    }\n\n\n    @Override\n    public void doUpdateVisitedHistory(WebView view, String url,\n                                       boolean isReload) {\n        if (mDelegate != null) {\n            mDelegate.doUpdateVisitedHistory(view, url, isReload);\n            return;\n        }\n        super.doUpdateVisitedHistory(view, url, isReload);\n    }\n\n    @Override\n    public void onReceivedSslError(WebView view, SslErrorHandler handler,\n                                   SslError error) {\n        if (mDelegate != null) {\n            mDelegate.onReceivedSslError(view, handler, error);\n            return;\n        }\n        super.onReceivedSslError(view, handler, error);\n    }\n\n    @Override\n    public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {\n        if (mDelegate != null) {\n            mDelegate.onReceivedClientCertRequest(view, request);\n            return;\n        }\n        super.onReceivedClientCertRequest(view, request);\n    }\n\n    @Override\n    public void onReceivedHttpAuthRequest(WebView view,\n                                          HttpAuthHandler handler, String host, String realm) {\n        if (mDelegate != null) {\n            mDelegate.onReceivedHttpAuthRequest(view, handler, host, realm);\n            return;\n        }\n        super.onReceivedHttpAuthRequest(view, handler, host, realm);\n    }\n\n    @Override\n    public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {\n        if (mDelegate != null) {\n            return mDelegate.shouldOverrideKeyEvent(view, event);\n        }\n        return super.shouldOverrideKeyEvent(view, event);\n    }\n\n    @Override\n    public void onUnhandledKeyEvent(WebView view, KeyEvent event) {\n        if (mDelegate != null) {\n            mDelegate.onUnhandledKeyEvent(view, event);\n            return;\n        }\n        super.onUnhandledKeyEvent(view, event);\n    }\n\n\n    @Override\n    public void onScaleChanged(WebView view, float oldScale, float newScale) {\n        if (mDelegate != null) {\n            mDelegate.onScaleChanged(view, oldScale, newScale);\n            return;\n        }\n        super.onScaleChanged(view, oldScale, newScale);\n    }\n\n    @Override\n    public void onReceivedLoginRequest(WebView view, String realm,\n                                       String account, String args) {\n        if (mDelegate != null) {\n            mDelegate.onReceivedLoginRequest(view, realm, account, args);\n            return;\n        }\n        super.onReceivedLoginRequest(view, realm, account, args);\n    }\n}\n"
  },
  {
    "path": "agentweb-core/src/main/res/layout/agentweb_error_page.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@color/white\"\n    >\n\n\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:text=\"@string/agentweb_default_page_error\"\n        android:textSize=\"22sp\"\n        android:gravity=\"center\"\n        android:textColor=\"#696969\"\n        />\n\n</LinearLayout>\n"
  },
  {
    "path": "agentweb-core/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"black\">#000000</color>\n    <color name=\"white\">#ffffff</color>\n    <color name=\"select_color\">#2e2e32</color>\n</resources>"
  },
  {
    "path": "agentweb-core/src/main/res/values/ids.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <item name=\"web_parent_layout_id\" type=\"id\"></item>\n    <item name=\"agentweb_webview_id\" type=\"id\"></item>\n    <item name=\"mainframe_error_viewsub_id\" type=\"id\"></item>\n    <item name=\"mainframe_error_container_id\" type=\"id\"></item>\n</resources>"
  },
  {
    "path": "agentweb-core/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"agentweb_download_task_has_been_exist\">The task already exists, do not repeat click to download!</string>\n    <string name=\"agentweb_tips\">Note</string>\n    <string name=\"agentweb_honeycomblow\">Wi-Fi disconnected. Continue the download via mobile data network?</string>\n    <string name=\"agentweb_download\">Download</string>\n    <string name=\"agentweb_cancel\">Cancel</string>\n    <string name=\"agentweb_download_fail\">Download failed!</string>\n    <string name=\"agentweb_current_downloading_progress\">Downloading:%s</string>\n    <string name=\"agentweb_current_downloaded_length\">downloaded:%s</string>\n    <string name=\"agentweb_trickter\">You have a new notice</string>\n    <string name=\"agentweb_file_download\">Download</string>\n    <string name=\"agentweb_click_open\">Tap to continue</string>\n    <string name=\"agentweb_coming_soon_download\">Coming soon to download the file</string>\n    <string name=\"agentweb_camera\">Camera</string>\n    <string name=\"agentweb_file_chooser\">Files</string>\n    <string name=\"agentweb_loading\">Loading ...</string>\n    <string name=\"agentweb_leave_app_and_go_other_page\">leaving %s and opening another app?</string>\n    <string name=\"agentweb_leave\">Go away</string>\n    <string name=\"agentweb_max_file_length_limit\">The selected file can not be larger than %s MB</string>\n    <string name=\"agentweb_default_page_error\">error~</string>\n</resources>\n"
  },
  {
    "path": "agentweb-core/src/main/res/values/style.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <style name=\"actionActivity\" parent=\"@android:style/Theme.Translucent.NoTitleBar\">\n        <item name=\"android:statusBarColor\" tools:targetApi=\"lollipop\">@android:color/transparent</item>\n    </style>\n\n</resources>"
  },
  {
    "path": "agentweb-core/src/main/res/values-zh/strings.xml",
    "content": "<resources>\n    <string name=\"agentweb_download_task_has_been_exist\">该任务已经存在 ， 请勿重复点击下载!</string>\n    <string name=\"agentweb_tips\">提示</string>\n    <string name=\"agentweb_honeycomblow\">您正在使用手机流量 ， 继续下载该文件吗?</string>\n    <string name=\"agentweb_download\">下载</string>\n    <string name=\"agentweb_cancel\">取消</string>\n    <string name=\"agentweb_download_fail\">下载失败!</string>\n    <string name=\"agentweb_current_downloading_progress\">当前进度:%s</string>\n    <string name=\"agentweb_current_downloaded_length\">已下载:%s</string>\n    <string name=\"agentweb_trickter\">您有一条新通知</string>\n    <string name=\"agentweb_file_download\">文件下载</string>\n    <string name=\"agentweb_click_open\">点击打开</string>\n    <string name=\"agentweb_coming_soon_download\">即将开始下载文件</string>\n    <string name=\"agentweb_camera\">相机</string>\n    <string name=\"agentweb_file_chooser\">文件</string>\n    <string name=\"agentweb_loading\">加载中 ...</string>\n    <string name=\"agentweb_leave_app_and_go_other_page\">您需要离开%s前往其他应用吗？</string>\n    <string name=\"agentweb_leave\">离开</string>\n    <string name=\"agentweb_max_file_length_limit\">选择的文件不能大于%sMB</string>\n    <string name=\"agentweb_default_page_error\">出错啦! 点击空白处刷新 ~</string>\n</resources>\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build/\n/src/androidTest/\n/src/test/\n/release/\n"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion 29\n    buildToolsVersion '29.0.3'\n\n    defaultConfig {\n        minSdkVersion 22\n        targetSdkVersion 29\n        applicationId \"me.wizos.loread\"\n        versionCode 1\n        versionName \"1.0.0\"\n        ndk {\n            abiFilters 'armeabi-v7a', 'arm64-v8a'\n        }\n        // 解决 Error:Execution failed for task ‘:app:javaPreCompileDebug 问题\n        javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }\n    }\n    buildTypes {\n        release {\n            minifyEnabled false //启动混淆\n            // proguardFile是混淆使用的配置文件，这里是module根目录下的proguard-rules.pro文件\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n            // AAI4F2S2LM1U 属于应用\"知微\"独有的 Android AppKey, 用于配置SDK；标注应用推广渠道用以区分新用户来源，可填写如应用宝，豌豆荚等\n            manifestPlaceholders = [MTA_APPKEY : \"AAI4F2S2LM1U\", MTA_CHANNEL: \"酷安\"]\n        }\n        debug {\n            //applicationIdSuffix '.baimu' //增加包名后缀\n            minifyEnabled false\n            debuggable true\n            manifestPlaceholders = [MTA_APPKEY : \"AAI4F2S2LM1U\", MTA_CHANNEL: \"酷安\"]\n        }\n    }\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n}\n\ndependencies {\n    implementation fileTree(include: ['*.jar'], dir: 'libs')\n    // 开发工具\n    testImplementation 'junit:junit:4.13'\n    implementation 'androidx.annotation:annotation:1.1.0'\n    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'\n    implementation 'androidx.appcompat:appcompat:1.1.0'\n    implementation 'androidx.legacy:legacy-support-v4:1.0.0'\n    implementation 'androidx.cardview:cardview:1.0.0'\n    implementation 'androidx.recyclerview:recyclerview:1.1.0'\n    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'\n    implementation 'com.google.android.material:material:1.1.0'\n    implementation \"androidx.paging:paging-runtime:2.1.2\"\n    implementation \"androidx.work:work-runtime:2.3.4\"\n\n    // 通用的背景设置库，通过标签直接生成shape，无需再写shape.xml [https://github.com/JavaNoober/BackgroundLibrary]\n    implementation 'com.noober.background:core:1.6.3'\n    // 列表项左右滑动布局\n    implementation project(path: ':swipelayout')\n    implementation project(path: ':support')\n    // 加载 view [https://github.com/ybq/Android-SpinKit]\n    // implementation 'com.github.ybq:Android-SpinKit:1.4.0'\n    // 浏览器\n    implementation project(path: ':agentweb-core')\n\n    // https://github.com/drakeet/MultiType, 'com.github.kelinZhou:MultiTypeAdapter:1.0.2'\n    implementation 'com.drakeet.multitype:multitype:4.2.0'\n\n    // 对话框\n    // implementation 'com.kongzue.dialog_v3x:dialog:3.1.6'\n    // implementation 'com.orhanobut:dialogplus:1.11@aar'\n    // implementation 'com.afollestad.material-dialogs:core:3.1.1'\n    implementation 'com.afollestad.material-dialogs:core:0.9.6.0'\n    implementation 'com.afollestad.material-dialogs:commons:0.9.6.0'\n\n    // 透明通知栏\n    // implementation 'com.readystatesoftware.systembartint:systembartint:1.0.3'\n\n    // popup [https://github.com/li-xiaojun/XPopup]\n    implementation 'com.lxj:xpopup:1.8.13'\n    // toast [https://github.com/getActivity/ToastUtils]\n    implementation 'com.hjq:toast:8.0'\n\n    // 开关按钮\n    implementation 'com.kyleduo.switchbutton:library:2.0.0'\n\n    // 让播放、暂停按钮优雅的过渡 [https://github.com/Lauzy/PlayPauseView]\n    implementation 'com.github.Lauzy:PlayPauseView:1.0.7'\n\n    // 带删除功能的EditText；显示或者隐藏密码；可设置自动添加分隔符分割电话号码、银行卡号等；支持禁止Emoji表情符号输入 [https://github.com/woxingxiao/XEditText]\n    // 用在了 activity_search.xml 中\n    implementation 'com.xw.repo:xedittext-androidx:2.2.6@aar'\n    // [https://github.com/zhanghai/materialedittext]\n    // 用在了 activity_login_tiny_tiny_rss_rss.xml 中\n    implementation 'me.zhanghai.android.materialedittext:library:1.0.5'\n\n    // 时间总线\n    // implementation 'org.greenrobot:eventbus:3.1.1'\n    implementation 'com.jeremyliao:live-event-bus-x:1.6.1'\n\n    implementation \"androidx.room:room-runtime:2.2.5\"\n    annotationProcessor \"androidx.room:room-compiler:2.2.5\"\n    // implementation 'com.tencent.wcdb:room:1.0.8'  // 代替 room-runtime，同时也不需要再引用 wcdb-android\n    // annotationProcessor 'android.arch.persistence.room:compiler:1.1.1' // compiler 需要用 room 的\n\n    // 基于 mmap 的高性能通用 key-value 组件\n    // implementation 'com.tencent:mmkv:1.1.0'\n\n    // 内存泄漏检测工具\n    // debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'\n    // releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'\n\n    // 日志工具\n    implementation 'com.orhanobut:logger:2.2.0'\n    implementation 'com.github.zhaokaiqiang.klog:library:1.6.0'\n\n    // 权限申请 [https://github.com/getActivity/XXPermissions]\n    implementation 'com.hjq:xxpermissions:6.2'\n\n    // 其中latest.release指代最新Bugly SDK版本号，也可以指定明确的版本号，例如:2.6.6.1\n    implementation 'com.tencent.bugly:crashreport:3.1.0'\n    // 腾讯统计MTA主包\n    implementation 'com.qq.mta:mta:3.4.7-release'\n    // 腾讯统计MID基础包\n    implementation 'com.tencent.mid:mid:4.06-Release'\n\n    // 依赖注入\n    implementation 'com.jakewharton:butterknife:10.2.1'\n    annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'\n\n    // 序列化数据 [https://github.com/google/gson]\n    implementation 'com.google.code.gson:gson:2.8.6'\n    implementation 'org.parceler:parceler-api:1.1.13'\n    annotationProcessor 'org.parceler:parceler:1.1.13'\n\n    // 抽取的正文没有html标签\n    // implementation 'com.chimbori.crux:crux:2.2.0'\n    // 正文抽取器\n    // implementation project(path: ':extractor')\n\n    // HTML 解析 [https://github.com/jhy/jsoup]\n    implementation 'org.jsoup:jsoup:1.13.1'\n    // HTML 解析\n    // implementation ('com.virjar:sipsoup:1.6'){ transitive = false }\n    // 使用xpath解析提取html数据 [https://github.com/zhegexiaohuozi/JsoupXpath]\n    // implementation 'cn.wanghaomiao:JsoupXpath:2.3.2'\n    // JSON 选择库 [https://github.com/json-path/JsonPath]\n    // implementation 'com.jayway.jsonpath:json-path:2.4.0'\n    // JS 库 [https://github.com/APISENSE/rhino-android]\n    implementation 'io.apisense:rhino-android:1.1.1'\n\n    // [https://github.com/bumptech/glide]\n    implementation 'com.github.bumptech.glide:glide:4.11.0'\n    annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'\n    // Glide 底层使用okhttp3\n    implementation('com.github.bumptech.glide:okhttp3-integration:4.11.0') { transitive = false }\n    // 查看大图 [https://github.com/SherlockGougou/BigImageViewPager]\n    implementation 'com.github.SherlockGougou:BigImageViewPager:androidx-6.0.1'\n\n    // 音频部件 [https://github.com/yhaolpz/FloatWindow]\n    implementation project(path: ':floatwindow')\n    // https://github.com/SDKers/FloatWindow, https://github.com/fenggit/FloatWindow\n\n    // retrofit2\n    implementation('com.squareup.retrofit2:retrofit:2.8.1') { transitive = false }\n    implementation('com.squareup.retrofit2:converter-gson:2.8.1') { transitive = false }\n    // okHttp\n    implementation 'com.squareup.okhttp3:okhttp:4.4.1'\n    implementation 'com.squareup.okio:okio:2.5.0'\n    implementation files('libs/okgo-3.0.4.jar')\n\n    // 网络状态监听 [https://github.com/allenlzhang/NetworkState]\n    implementation 'com.github.allenlzhang:NetworkState:v1.0.0'\n    // 图片压缩 [https://github.com/Curzibn/Luban]\n    implementation project(path: ':luban')\n\n    // 防止三方 SDK 中常见的损害用户体验的行为 [https://github.com/oasisfeng/condom]\n    implementation 'com.oasisfeng.condom:library:2.5.0'\n\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard replaceUrl here.\n# By default, the flags in this file are appended to flags specified\n# in C:\\Program Files\\Android\\sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class userName to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n"
  },
  {
    "path": "app/src/androidTest/java/me/wizos/loread/ApplicationTest.java",
    "content": "package me.wizos.loread;\n\nimport android.app.Application;\nimport android.test.ApplicationTestCase;\n\n/**\n * <a href=\"http://d.android.com/tools/testing/testing_android.html\">Testing Fundamentals</a>\n */\npublic class ApplicationTest extends ApplicationTestCase<Application> {\n    public ApplicationTest() {\n        super(Application.class);\n    }\n}"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"me.wizos.loread\">\n    <!-- 允许访问振动设备 -->\n    <uses-permission android:name=\"android.permission.VIBRATE\" />\n    <!-- 网络相关权限 -->\n    <uses-permission android:name=\"android.permission.INTERNET\" /> <!-- MTA统计必选权限 -->\n\n    <!-- 查看帐户需要权限 -->\n    <uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>\n    <!-- 添加帐户需要权限 -->\n    <uses-permission android:name=\"android.permission.AUTHENTICATE_ACCOUNTS\"/>\n    <uses-permission\n        android:name=\"android.permission.MANAGE_ACCOUNTS\"\n        android:maxSdkVersion=\"22\" />\n    <uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\" />\n\n    <uses-permission android:name=\"android.permission.WRITE_SYNC_SETTINGS\" />\n\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n    <uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>\n    <uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>\n    <uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>\n    <uses-permission android:name=\"android.permission.READ_PHONE_STATE\" /> <!-- 访问电话状态 -->\n    <!-- <uses-permission android:name=\"android.permission.WRITE_SETTINGS\" /> --><!-- 允许读写系统设置项 -->\n    <!-- 挂载、反挂载外部文件系统 -->\n    <!-- <uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\" /> -->\n    <!-- 读取系统底层日志 -->\n    <!-- <uses-permission android:name=\"android.permission.READ_LOGS\" /> -->\n    <!-- 允许程序在手机屏幕关闭后后台进程仍然运行 -->\n    <!-- <uses-permission andruserNamename=\"android.permission.WAKE_LOCK\" /> -->\n    <uses-permission android:name=\"android.permission.SYSTEM_OVERLAY_WINDOW\" /> <!-- <uses-permission android:name=\"android.permission.GET_TASKS\"/> -->\n    <!-- [尝试] 防止左右滑动时的报错 -->\n    <uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\" /> <!-- 通过WiFi或移动基站的方式获取用户错略的经纬度信息，定位精度大概误差在30~1500米 -->\n    <!-- <uses-permission andruserNamename=\"android.permission.ACCESS_COARSE_LOCATION\" /> -->\n    <!-- 读取，写出外置SD卡内容的权限该目录 -->\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\" />\n    <uses-permission android:name=\"android.permission.WAKE_LOCK\" />\n\n\n    <application\n        android:name=\".App\"\n        android:allowBackup=\"true\"\n        android:appComponentFactory=\"me.wizos.loread\"\n        android:requestLegacyExternalStorage=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:largeHeap=\"true\"\n        android:networkSecurityConfig=\"@xml/network_security_config\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme.Day\"\n        tools:replace=\"android:networkSecurityConfig,android:appComponentFactory\"\n        tools:targetApi=\"q\">\n        <meta-data\n            android:name=\"me.wizos.loread.network.glide.OkHttpAppGlideModule\"\n            android:value=\"AppGlideModule\" />\n\n\n        <activity\n            android:label=\"@string/app_name\"\n            android:name=\".activity.login.LoginTinyRSSActivity\"\n            android:parentActivityName=\".activity.ProviderActivity\"\n            android:configChanges=\"orientation|screenSize|keyboardHidden\"\n            android:launchMode=\"singleTask\"\n            android:screenOrientation=\"user\"\n            android:theme=\"@style/AppTheme.Day.NoActionBar\"/>\n\n        <activity\n            android:label=\"@string/app_name\"\n            android:name=\".activity.login.LoginInoReaderActivity\"\n            android:parentActivityName=\".activity.ProviderActivity\"\n            android:configChanges=\"orientation|screenSize|keyboardHidden\"\n            android:launchMode=\"singleTask\"\n            android:screenOrientation=\"user\"\n            android:theme=\"@style/AppTheme.Day.NoActionBar\"/>\n\n        <activity\n            android:name=\".activity.SplashActivity\"\n            android:label=\"@string/app_name\"\n            android:launchMode=\"singleTask\"\n            android:screenOrientation=\"user\"\n            android:theme=\"@style/AppBaseTheme.SplashTheme\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n\n        <activity\n            android:name=\".activity.ProviderActivity\"\n            android:configChanges=\"orientation|screenSize\"\n            android:label=\"@string/app_name\"\n            android:launchMode=\"singleTask\"\n            android:screenOrientation=\"user\"\n            android:theme=\"@style/AppTheme.Day.NoActionBar\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:host=\"oauth\"\n                    android:scheme=\"loread\" />\n                <data\n                    android:host=\"oauth_inoreader\"\n                    android:scheme=\"loread\" />\n                <data\n                    android:host=\"feedlyauth\"\n                    android:scheme=\"palabre\" />\n                <data\n                    android:host=\"auth\"\n                    android:scheme=\"pby\" />\n                <data\n                    android:host=\"oauth\"\n                    android:scheme=\"feedme\" />\n            </intent-filter>\n        </activity>\n\n        <activity\n            android:name=\".activity.MainActivity\"\n            android:configChanges=\"orientation|screenSize|keyboardHidden\"\n            android:label=\"@string/app_name\"\n            android:launchMode=\"singleTask\"\n            android:screenOrientation=\"user\"\n            android:theme=\"@style/AppTheme.Day.NoActionBar\" />\n\n        <activity\n            android:name=\".activity.ArticleActivity\"\n            android:configChanges=\"orientation|screenSize|keyboardHidden\"\n            android:label=\"@string/app_name\"\n            android:launchMode=\"singleTask\"\n            android:parentActivityName=\".activity.MainActivity\"\n            android:screenOrientation=\"user\"\n            android:theme=\"@style/AppTheme.Day.NoActionBar\"\n            android:windowSoftInputMode=\"adjustResize|stateHidden\" />\n        <activity\n            android:name=\".activity.MusicActivity\"\n            android:configChanges=\"orientation|screenSize|keyboardHidden\"\n            android:label=\"@string/music\"\n            android:launchMode=\"singleTask\"\n            android:screenOrientation=\"user\"\n            android:theme=\"@style/AppTheme.Day.NoActionBar\"\n            android:windowSoftInputMode=\"adjustResize|stateHidden\" />\n\n        <activity\n            android:name=\".activity.TTSActivity\"\n            android:configChanges=\"orientation|screenSize|keyboardHidden\"\n            android:label=\"@string/speak\"\n            android:launchMode=\"singleTask\"\n            android:screenOrientation=\"user\"\n            android:theme=\"@style/AppTheme.Day.NoActionBar\"\n            android:windowSoftInputMode=\"adjustResize|stateHidden\" />\n\n        <!-- 最好不要注册为系统浏览器组件，不然当被设为系统默认的浏览器时，因为我没有处理添加下载监听，和默认打开系统浏览器的事件，会导致循环在这个页面 -->\n        <activity\n            android:name=\".activity.WebActivity\"\n            android:configChanges=\"orientation|screenSize|keyboardHidden\"\n            android:hardwareAccelerated=\"true\"\n            android:launchMode=\"singleTop\"\n            android:theme=\"@style/AppBaseTheme.SplashTranslucentTheme\" />\n\n        <activity\n            android:name=\".activity.SearchActivity\"\n            android:launchMode=\"singleTop\"\n            android:parentActivityName=\".activity.MainActivity\"\n            android:screenOrientation=\"user\"\n            android:theme=\"@style/AppTheme.Day.NoActionBar\" />\n        <activity\n            android:name=\".activity.SettingActivity\"\n            android:label=\"@string/settings\"\n            android:launchMode=\"singleTask\"\n            android:parentActivityName=\".activity.MainActivity\"\n            android:screenOrientation=\"user\"\n            android:theme=\"@style/AppTheme.Day.NoActionBar\" />\n\n<!--        <receiver android:name=\".service.NetworkStateReceiver\">-->\n<!--            <intent-filter>-->\n<!--                <action android:name=\"android.net.conn.CONNECTIVITY_CHANGE\" />-->\n\n<!--                <category android:name=\"android.intent.category.DEFAULT\" />-->\n<!--            </intent-filter>-->\n<!--        </receiver>-->\n\n        <activity\n            android:name=\".activity.LabActivity\"\n            android:configChanges=\"orientation|screenSize|keyboardHidden\"\n            android:hardwareAccelerated=\"true\"\n            android:launchMode=\"singleTop\" />\n        <activity android:name=\".activity.RuleGenerateActivity\" />\n        <activity\n            android:name=\".activity.FeedActivity\"\n            android:label=\"@string/title_activity_scrolling\"\n            android:theme=\"@style/AppTheme.Day.NoActionBar\" />\n\n\n        <service\n            android:name=\".service.MusicService\"\n            android:exported=\"false\"\n            android:permission=\"android.permission.BIND_SERVICE\" />\n        <service\n            android:name=\".service.AudioService\"\n            android:exported=\"false\"\n            android:permission=\"android.permission.BIND_SERVICE\" />\n\n<!--        <service-->\n<!--            android:name=\".service.MainService\"-->\n<!--            android:exported=\"false\"-->\n<!--            android:permission=\"android.permission.BIND_JOB_SERVICE\" />-->\n<!--        <service-->\n<!--            android:name=\".account.AccountAuthenticatorService\"-->\n<!--            android:exported=\"false\" >-->\n<!--            <intent-filter>-->\n<!--                <action android:name=\"android.accounts.AccountAuthenticator\" />-->\n<!--            </intent-filter>-->\n<!--            <meta-data-->\n<!--                android:name=\"android.accounts.AccountAuthenticator\"-->\n<!--                android:resource=\"@xml/account_authenticator\" />-->\n<!--        </service>-->\n\n<!--        <service-->\n<!--            android:name=\".account.SyncService\"-->\n<!--            android:exported=\"true\">-->\n<!--            <intent-filter>-->\n<!--                <action android:name=\"android.content.SyncAdapter\" />-->\n<!--            </intent-filter>-->\n<!--            <meta-data-->\n<!--                android:name=\"android.content.SyncAdapter\"-->\n<!--                android:resource=\"@xml/account_sync_adapter\" />-->\n<!--            &lt;!&ndash; resource属性指定一定说明该同步基本显示信息的xml文件 &ndash;&gt;-->\n<!--        </service>-->\n<!--        <provider-->\n<!--            android:name=\".account.AccountProvider\"-->\n<!--            android:authorities=\"me.wizos.loreadx.account.provide\"-->\n<!--            android:exported=\"false\"-->\n<!--            android:syncable=\"true\"/>-->\n\n<!--        <service-->\n<!--            android:name=\".account.CrazyJobService\"-->\n<!--            android:permission=\"android.permission.BIND_JOB_SERVICE\"/>-->\n\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/assets/css/android_studio.css",
    "content": "/*\nDate: 24 Fev 2015\nAuthor: Pedro Oliveira <kanytu@gmail . com>\n*/\n\n.hljs {\n  color: #a9b7c6;\n  background: #282b2e;\n  display: block;\n  overflow-x: auto;\n  padding: 0.5em;\n}\n\n.hljs-number,\n.hljs-literal,\n.hljs-symbol,\n.hljs-bullet {\n  color: #6897BB;\n}\n\n.hljs-keyword,\n.hljs-selector-tag,\n.hljs-deletion {\n  color: #cc7832;\n}\n\n.hljs-variable,\n.hljs-template-variable,\n.hljs-link {\n  color: #629755;\n}\n\n.hljs-comment,\n.hljs-quote {\n  color: #808080;\n}\n\n.hljs-meta {\n  color: #bbb529;\n}\n\n.hljs-string,\n.hljs-attribute,\n.hljs-addition {\n  color: #6A8759;\n}\n\n.hljs-section,\n.hljs-title,\n.hljs-type {\n  color: #ffc66d;\n}\n\n.hljs-name,\n.hljs-selector-id,\n.hljs-selector-class {\n  color: #e8bf6a;\n}\n\n.hljs-emphasis {\n  font-style: italic;\n}\n\n.hljs-strong {\n  font-weight: bold;\n}\n"
  },
  {
    "path": "app/src/main/assets/css/article_theme_day.css",
    "content": "/* #f8f8f8;灰白\n    background-color:rgba(0,0,0,0);*/\n\nbody {\n    //color: #333;\ncolor:#293845;\n    background-color: #FFFFFF;\n}\n/* 标题\n=============================================================================*/\n/*\nh1,h2 {\n    color: #000;\n    border-bottom: 1px dashed #ccc;\n}\n\nhr {\n    //background-color:#f8f8f8;\n}\n*/\n\n/* 表格\n=============================================================================*/\ntable{\n    background-color: #f8f8f8;\n}\ntable th, table td {\n    border: 1px solid #ccc;\n}\n\n/* 代码\n=============================================================================*/\ncode, tt ,pre {\n    border: 1px solid #eaeaea;\n    background-color: #f8f8f8;\n}\nblockquote{\n\tbackground: rgba(0, 0, 0, 0.04) !important;\n\tcolor: rgba(0, 0, 0, 0.5) !important;\n}"
  },
  {
    "path": "app/src/main/assets/css/article_theme_night.css",
    "content": "::-webkit-scrollbar {\n    width: 8px;\n    height: 8px;\n    background: #2c3e50;\n}\n\n::-webkit-scrollbar-thumb {\n    background: #888;\n}\n\na {\n    color: #c6cddb;\n}\n\nbody{\n    color: #c6cddb;\n    //background: #494f5c;\n    background-color: #454952;\n}\n\n\n/* 表格\n=============================================================================*/\n    /*background-color: #141e21;\n    border: 1px solid #203238;*/\npre {\n    color: #eee;\n    background: #2c3e50; \n}\n\ncode {\n\tcolor: #eee;\n\tbackground: #7d828a;\n}\ntable{\n    color: #eee;\n\tborder: 1px solid rgba(0, 0, 0, 0.1) !important;\n\tbackground: rgba(0, 0, 0, 0.08) !important;\n    /*background-color: #203238;*/\n}\ntt {\n    background-color: #141e21;\n    border: 1px solid #203238;\n}\n\ntable th, table td {\n    //border: 1px solid #212121;\n\tborder: 1px solid rgba(0, 0, 0, 0.1);\n}\n\n/* 降低亮度 */\nimg,video,iframe,embed {\n    -webkit-filter:brightness(0.75);\n}\nblockquote{\n\tbackground: rgba(0, 0, 0, 0.08) !important;\n\tborder-left: 5px solid rgba(0, 0, 0, 0.3) !important;\n\tcolor: rgba(255, 255, 255, 0.6) !important;\n}\n\n"
  },
  {
    "path": "app/src/main/assets/css/normalize.css",
    "content": "/* 重置 */\nhtml, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n}\n:focus{outline:none}\n\n@font-face {\n\tfont-family: 'roboto_light';\n\tsrc: url('file:///android_asset/fonts/roboto_light.ttf');\n}\n\nbody {\n\tpadding: 20px 15px !important;\n\tword-wrap: break-word !important;\n}\n\nbody,div,p,font {\n\tfont-family: roboto_light,sans-serif !important;\n\tfont-size: 16px;\n\tline-height: 160% !important;\n\tletter-spacing:1px;\n}\n\nbody > *:first-child {\n\tmargin-top: 0 !important;\n}\nbody > *:last-child {\n\tmargin-bottom: 0 !important;\n}\nbody * {\n\tmax-width: 100% !important;\n}\n\n/* BLOCKS */\np, ul, ol, dl, table, pre, figure {\n\tmargin: 10px 0;\n}\ntable pre{\n\tmargin: 0px 0;\n}\n\n\n/*\n * 设置最大宽度，防止超出屏幕\n */\ndiv,p,tbody,textarea,ins,input{\n\tmax-width: 100% !important;\n}\n\n/* \n * 如果表格内的div宽度过长，设置最大宽度是无法防止溢出的，所以得手动将其宽度改为自适应\n */\ntable div{\n\twidth:auto !important;\n}\n\n\n* {\n\tbackground-image:none !important;\n}\n\np{\n\ttext-indent:0 !important;\n\tbackground:none !important;\n\twidth:auto !important;\n\theight:auto !important;\n}\n\n/* HEADERS */\nh1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code, h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {\n\tfont-size: inherit;\n}\n\nsection h1,h2,h3,h4,h5,h6{\n\tfont-family: roboto_light !important;\n\tpadding: 0 0px !important;\n\tmargin: 15px 0px !important;\n}\n\nh4:before{\n\tcontent: \"⭕️\";\n}\nh5:before{\n\tcontent: \"💢\";\n}\nh6:before{\n\tcontent: \"🌀\";\n}\nsection h1 {\n\tfont-size: 24px;\n\tbox-shadow: inset 0 -8px 0 #bf53b1;\n}\nh2 {\n\tfont-size: 20px;\n\tbox-shadow: inset 0 -6px 0 #f7615f;\n}\nh3 {\n\tfont-size: 16px;\n\tbox-shadow: inset 0 -2px 0 #e0af55;\n}\nh4 {\n\tfont-size: 16px;\n\tcolor: #d2cd3e;\n}\nh5 {\n\tfont-size: 16px;\n\tcolor: #6AC749;\n}\nh6 {\n\tfont-size: 16px;\n\tcolor: #32b9a8;\n}\n\n\n/* LINKS */\na {\n\tcolor: #51a1f1 !important;\n}\na > svg{\n\twidth: 14px;\n\theight:auto;\n}\nsection a {\n\ttext-decoration: underline !important;\n}\n#title a {\n\ttext-decoration: none;\n}\na[href=''],a:not([href]) {\n\ttext-decoration: none !important;\n}\n\n/* 列表 */\nul, ol {\n\tpadding: 0 0 0 1em;\n\tlist-style-position:inside;\n}\n\nul li > :first-child,\nol li > :first-child,\nul li ul:first-of-type,\nol li ol:first-of-type,\nul li ol:first-of-type,\nol li ul:first-of-type {\n\tmargin-top: 0px;\n}\nul ul, ul ol, ol ol, ol ul {\n\tmargin-bottom: 0;\n}\nli{\n\tmargin: .2em 0;\n}\n/*\ndl {\n\tpadding: 0;\n}\n*/\ndl dt {\n\tfont-size: 14px;\n\tfont-weight: bold;\n\tfont-style: italic;\n\tmargin: 15px 0 5px;\n\t/*padding: 0;*/\n}\n\ndl dt:first-child {\n\tpadding: 0;\n}\n\ndl dt > :first-child {\n\tmargin-top: 0px;\n}\n\ndl dt > :last-child {\n\tmargin-bottom: 0px;\n}\n\ndl dd {\n\tmargin: 0 0 15px;\n\tpadding: 0 15px;\n}\n\ndl dd > :first-child {\n\tmargin-top: 0px;\n}\n\ndl dd > :last-child {\n\tmargin-bottom: 0px;\n}\n\n/* 表格 */\ntable {\n\twidth: auto !important;\n\theight: auto !important;\n\tmargin: 0 !important;\n\tborder-collapse: collapse !important;\n\tborder-spacing: 0;\n\tfont-size: .8em;\n\tline-height: 1.1;\n\tmax-width: 100% !important;\n\tpadding: .5em .5em;\n\toverflow: auto;\n\tword-wrap: break-word !important;\n\tletter-spacing: normal;\n\tborder-radius: 5px;\n}\ntable th {\n\tfont-weight: bold;\n}\ntable th,table td {\n\tpadding: 5px !important;\n\twidth: auto !important;\n\tmax-width: 100% !important;\n\theight: auto !important;\n}\nthead > tr, tbody > tr:nth-child(2n) {\n\tbackground: rgba(0, 0, 0, 0.06) !important;\n}\ntable p{\n\tmargin: 5px 0;\n}\n/* 垂直居中 */\nth img, td img{\n\tvertical-align:middle;\n}\n/* 对齐是排版最重要的因素, 别让什么都居中 */\ncaption, th {\n\ttext-align: left;\n}\n\ntd pre, td code,pre code, pre tt {\n\tborder: 0 !important;\n\tbackground: transparent  !important;\n}\n\n/* CODE */\npre code, pre, code, tt {\n\tfont-family: Consolas, Liberation Mono Courier, monospace;\n}\npre {\n\tfont-size: .8em;\n\tline-height: 1.5;\n\tmax-width: 100% !important;\n\tpadding: .5em .5em;\n\toverflow: auto;\n\tword-wrap: break-word !important;\n\tletter-spacing: normal;\n\twhite-space: pre;\n\tborder-radius: 5px;\n\ttext-align: left;\n}\npre code,table pre{\n\tfont-size: .8em;\n}\ncode {\n\tfont-size: .8em;\n\tborder-radius: 3px;\n\tpadding: 0 3px;\n\tmargin: 0 3px;\n\tword-break: break-all;\n\tletter-spacing:normal;\n}\ntt {\n\tmargin: 0 0px;\n\tpadding: 0 0px;\n}\n\n/* QUOTES */\nblockquote {\n\tfont-size: .8em !important;\n\tpadding: 5px 5px 5px 15px !important;\n\tmargin: 15px 0 15px 0px !important;\n}\nblockquote > :first-child {\n\tmargin-top: 0px !important;\n}\nblockquote > :last-child {\n\tmargin-bottom: 0px !important;\n}\n\n/* figure */\nfigure {\n\tmax-width: 100%;\n\theight: auto;\n\ttext-align: center;\n}\nfigcaption {\n\tfont-size: .8em;\n\tfont-style: italic;\n\topacity: .6;\n}\ntextarea{\n\tdisplay:none;\n}\n\n/* 图片，视频 */\n/* 隐藏\"空/无效\"元素 */\nimg[src=''],audio:not([src]),iframe:not([src]),[src=''],blockquote:empty,figure:empty,table:empty,pre:empty,code:empty,div:empty {\n\tdisplay: none !important;\n}\n\nimg {\n\tmargin: 5px 0 !important;\n\tmax-width: 100% !important;\n\twidth: auto;\n\theight: auto !important;\n\tborder-radius: 5px;\n}\n\na > img{\n\tdisplay: block;\n}\nvideo,iframe,embed {\n\twidth: 100% !important;\n\theight: 100% !important;\n\tdisplay: block !important;\n}\n.video_wrap,.iframe_wrap,.embed_wrap {\n\tmargin: 5px 0 !important;\n\tborder-bottom: 5px solid rgba(0, 0, 0, 0.1) !important;\n\tborder-top: 5px solid rgba(0, 0, 0, 0.1) !important;\n\tborder-radius: 5px !important;\n\tbackground:repeating-linear-gradient(-50deg,#E8E8E8,#E8E8E8 1em,#d2d2d2 1em,#d2d2d2 2em) !important;\n}\n.table_wrap{\n\toverflow-x:auto !important;\n}\naudio {\n\theight: 32px;\n\twidth: 100% !important;\n\tmargin: 5px 0 !important;\n}\nvideo {\n    background:rgba(0, 0, 0);\n}\n\n\n/* 文章相关信息的排版*/\nhr {\n\topacity: .2;\n\tborder-width: 0 0 5px;\n\tborder-style: dashed;\n\tbackground: transparent;\n\twidth: 50%;\n\tmargin: .9em auto;\n}\n#title {\n\tfont-family:roboto_light,sans-serif !important;\n\tfont-size: 1.4em;\n\tmargin-bottom: 5px;\n}\n#author,#pubDate {\n\tfont-size: .8em !important;\n\tmargin: 0 !important;\n\topacity: 0.4;\n}\n#readability-button{\n\tfont-size:12px;\n\tmargin-top: 10px\n}\n#content{\n\tmargin-top: 10px\n}\n\ndel{\n\topacity:0.4;\n}\n"
  },
  {
    "path": "app/src/main/assets/js/highlight.pack.js",
    "content": "/*! highlight.js v9.18.1 | BSD3 License | git.io/hljslicense */\n!function(e){var n=\"object\"==typeof window&&window||\"object\"==typeof self&&self;\"undefined\"==typeof exports||exports.nodeType?n&&(n.hljs=e({}),\"function\"==typeof define&&define.amd&&define([],function(){return n.hljs})):e(exports)}(function(a){var f=[],o=Object.keys,_={},g={},C=!0,n=/^(no-?highlight|plain|text)$/i,E=/\\blang(?:uage)?-([\\w-]+)\\b/i,t=/((^(<[^>]+>|\\t|)+|(?:\\n)))/gm,r={case_insensitive:\"cI\",lexemes:\"l\",contains:\"c\",keywords:\"k\",subLanguage:\"sL\",className:\"cN\",begin:\"b\",beginKeywords:\"bK\",end:\"e\",endsWithParent:\"eW\",illegal:\"i\",excludeBegin:\"eB\",excludeEnd:\"eE\",returnBegin:\"rB\",returnEnd:\"rE\",variants:\"v\",IDENT_RE:\"IR\",UNDERSCORE_IDENT_RE:\"UIR\",NUMBER_RE:\"NR\",C_NUMBER_RE:\"CNR\",BINARY_NUMBER_RE:\"BNR\",RE_STARTERS_RE:\"RSR\",BACKSLASH_ESCAPE:\"BE\",APOS_STRING_MODE:\"ASM\",QUOTE_STRING_MODE:\"QSM\",PHRASAL_WORDS_MODE:\"PWM\",C_LINE_COMMENT_MODE:\"CLCM\",C_BLOCK_COMMENT_MODE:\"CBCM\",HASH_COMMENT_MODE:\"HCM\",NUMBER_MODE:\"NM\",C_NUMBER_MODE:\"CNM\",BINARY_NUMBER_MODE:\"BNM\",CSS_NUMBER_MODE:\"CSSNM\",REGEXP_MODE:\"RM\",TITLE_MODE:\"TM\",UNDERSCORE_TITLE_MODE:\"UTM\",COMMENT:\"C\",beginRe:\"bR\",endRe:\"eR\",illegalRe:\"iR\",lexemesRe:\"lR\",terminators:\"t\",terminator_end:\"tE\"},m=\"</span>\",O=\"Could not find the language '{}', did you forget to load/include a language module?\",B={classPrefix:\"hljs-\",tabReplace:null,useBR:!1,languages:void 0},c=\"of and for in not or if then\".split(\" \");function x(e){return e.replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\")}function d(e){return e.nodeName.toLowerCase()}function R(e){return n.test(e)}function i(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function p(e){var a=[];return function e(n,t){for(var r=n.firstChild;r;r=r.nextSibling)3===r.nodeType?t+=r.nodeValue.length:1===r.nodeType&&(a.push({event:\"start\",offset:t,node:r}),t=e(r,t),d(r).match(/br|hr|img|input/)||a.push({event:\"stop\",offset:t,node:r}));return t}(e,0),a}function v(e,n,t){var r=0,a=\"\",i=[];function o(){return e.length&&n.length?e[0].offset!==n[0].offset?e[0].offset<n[0].offset?e:n:\"start\"===n[0].event?e:n:e.length?e:n}function c(e){a+=\"<\"+d(e)+f.map.call(e.attributes,function(e){return\" \"+e.nodeName+'=\"'+x(e.value).replace(/\"/g,\"&quot;\")+'\"'}).join(\"\")+\">\"}function l(e){a+=\"</\"+d(e)+\">\"}function u(e){(\"start\"===e.event?c:l)(e.node)}for(;e.length||n.length;){var s=o();if(a+=x(t.substring(r,s[0].offset)),r=s[0].offset,s===e){for(i.reverse().forEach(l);u(s.splice(0,1)[0]),(s=o())===e&&s.length&&s[0].offset===r;);i.reverse().forEach(c)}else\"start\"===s[0].event?i.push(s[0].node):i.pop(),u(s.splice(0,1)[0])}return a+x(t.substr(r))}function l(n){return n.v&&!n.cached_variants&&(n.cached_variants=n.v.map(function(e){return i(n,{v:null},e)})),n.cached_variants?n.cached_variants:function e(n){return!!n&&(n.eW||e(n.starts))}(n)?[i(n,{starts:n.starts?i(n.starts):null})]:Object.isFrozen(n)?[i(n)]:[n]}function u(e){if(r&&!e.langApiRestored){for(var n in e.langApiRestored=!0,r)e[n]&&(e[r[n]]=e[n]);(e.c||[]).concat(e.v||[]).forEach(u)}}function M(n,t){var i={};return\"string\"==typeof n?r(\"keyword\",n):o(n).forEach(function(e){r(e,n[e])}),i;function r(a,e){t&&(e=e.toLowerCase()),e.split(\" \").forEach(function(e){var n,t,r=e.split(\"|\");i[r[0]]=[a,(n=r[0],(t=r[1])?Number(t):function(e){return-1!=c.indexOf(e.toLowerCase())}(n)?0:1)]})}}function S(r){function s(e){return e&&e.source||e}function f(e,n){return new RegExp(s(e),\"m\"+(r.cI?\"i\":\"\")+(n?\"g\":\"\"))}function a(a){var i,e,o={},c=[],l={},t=1;function n(e,n){o[t]=e,c.push([e,n]),t+=new RegExp(n.toString()+\"|\").exec(\"\").length-1+1}for(var r=0;r<a.c.length;r++){n(e=a.c[r],e.bK?\"\\\\.?(?:\"+e.b+\")\\\\.?\":e.b)}a.tE&&n(\"end\",a.tE),a.i&&n(\"illegal\",a.i);var u=c.map(function(e){return e[1]});return i=f(function(e,n){for(var t=/\\[(?:[^\\\\\\]]|\\\\.)*\\]|\\(\\??|\\\\([1-9][0-9]*)|\\\\./,r=0,a=\"\",i=0;i<e.length;i++){var o=r+=1,c=s(e[i]);for(0<i&&(a+=n),a+=\"(\";0<c.length;){var l=t.exec(c);if(null==l){a+=c;break}a+=c.substring(0,l.index),c=c.substring(l.index+l[0].length),\"\\\\\"==l[0][0]&&l[1]?a+=\"\\\\\"+String(Number(l[1])+o):(a+=l[0],\"(\"==l[0]&&r++)}a+=\")\"}return a}(u,\"|\"),!0),l.lastIndex=0,l.exec=function(e){var n;if(0===c.length)return null;i.lastIndex=l.lastIndex;var t=i.exec(e);if(!t)return null;for(var r=0;r<t.length;r++)if(null!=t[r]&&null!=o[\"\"+r]){n=o[\"\"+r];break}return\"string\"==typeof n?(t.type=n,t.extra=[a.i,a.tE]):(t.type=\"begin\",t.rule=n),t},l}if(r.c&&-1!=r.c.indexOf(\"self\")){if(!C)throw new Error(\"ERR: contains `self` is not supported at the top-level of a language.  See documentation.\");r.c=r.c.filter(function(e){return\"self\"!=e})}!function n(t,e){t.compiled||(t.compiled=!0,t.k=t.k||t.bK,t.k&&(t.k=M(t.k,r.cI)),t.lR=f(t.l||/\\w+/,!0),e&&(t.bK&&(t.b=\"\\\\b(\"+t.bK.split(\" \").join(\"|\")+\")\\\\b\"),t.b||(t.b=/\\B|\\b/),t.bR=f(t.b),t.endSameAsBegin&&(t.e=t.b),t.e||t.eW||(t.e=/\\B|\\b/),t.e&&(t.eR=f(t.e)),t.tE=s(t.e)||\"\",t.eW&&e.tE&&(t.tE+=(t.e?\"|\":\"\")+e.tE)),t.i&&(t.iR=f(t.i)),null==t.relevance&&(t.relevance=1),t.c||(t.c=[]),t.c=Array.prototype.concat.apply([],t.c.map(function(e){return l(\"self\"===e?t:e)})),t.c.forEach(function(e){n(e,t)}),t.starts&&n(t.starts,e),t.t=a(t))}(r)}function T(n,e,a,t){var i=e;function c(e,n,t,r){if(!t&&\"\"===n)return\"\";if(!e)return n;var a='<span class=\"'+(r?\"\":B.classPrefix);return(a+=e+'\">')+n+(t?\"\":m)}function o(){p+=(null!=d.sL?function(){var e=\"string\"==typeof d.sL;if(e&&!_[d.sL])return x(v);var n=e?T(d.sL,v,!0,R[d.sL]):w(v,d.sL.length?d.sL:void 0);return 0<d.relevance&&(M+=n.relevance),e&&(R[d.sL]=n.top),c(n.language,n.value,!1,!0)}:function(){var e,n,t,r,a,i,o;if(!d.k)return x(v);for(r=\"\",n=0,d.lR.lastIndex=0,t=d.lR.exec(v);t;)r+=x(v.substring(n,t.index)),a=d,i=t,o=g.cI?i[0].toLowerCase():i[0],(e=a.k.hasOwnProperty(o)&&a.k[o])?(M+=e[1],r+=c(e[0],x(t[0]))):r+=x(t[0]),n=d.lR.lastIndex,t=d.lR.exec(v);return r+x(v.substr(n))})(),v=\"\"}function l(e){p+=e.cN?c(e.cN,\"\",!0):\"\",d=Object.create(e,{parent:{value:d}})}function u(e){var n=e[0],t=e.rule;return t&&t.endSameAsBegin&&(t.eR=new RegExp(n.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g,\"\\\\$&\"),\"m\")),t.skip?v+=n:(t.eB&&(v+=n),o(),t.rB||t.eB||(v=n)),l(t),t.rB?0:n.length}function s(e){var n=e[0],t=i.substr(e.index),r=function e(n,t){if(r=n.eR,a=t,(i=r&&r.exec(a))&&0===i.index){for(;n.endsParent&&n.parent;)n=n.parent;return n}var r,a,i;if(n.eW)return e(n.parent,t)}(d,t);if(r){var a=d;for(a.skip?v+=n:(a.rE||a.eE||(v+=n),o(),a.eE&&(v=n));d.cN&&(p+=m),d.skip||d.sL||(M+=d.relevance),(d=d.parent)!==r.parent;);return r.starts&&(r.endSameAsBegin&&(r.starts.eR=r.eR),l(r.starts)),a.rE?0:n.length}}var f={};function r(e,n){var t=n&&n[0];if(v+=e,null==t)return o(),0;if(\"begin\"==f.type&&\"end\"==n.type&&f.index==n.index&&\"\"===t)return v+=i.slice(n.index,n.index+1),1;if(\"begin\"===(f=n).type)return u(n);if(\"illegal\"===n.type&&!a)throw new Error('Illegal lexeme \"'+t+'\" for mode \"'+(d.cN||\"<unnamed>\")+'\"');if(\"end\"===n.type){var r=s(n);if(null!=r)return r}return v+=t,t.length}var g=D(n);if(!g)throw console.error(O.replace(\"{}\",n)),new Error('Unknown language: \"'+n+'\"');S(g);var E,d=t||g,R={},p=\"\";for(E=d;E!==g;E=E.parent)E.cN&&(p=c(E.cN,\"\",!0)+p);var v=\"\",M=0;try{for(var b,h,N=0;d.t.lastIndex=N,b=d.t.exec(i);)h=r(i.substring(N,b.index),b),N=b.index+h;for(r(i.substr(N)),E=d;E.parent;E=E.parent)E.cN&&(p+=m);return{relevance:M,value:p,i:!1,language:n,top:d}}catch(e){if(e.message&&-1!==e.message.indexOf(\"Illegal\"))return{i:!0,relevance:0,value:x(i)};if(C)return{relevance:0,value:x(i),language:n,top:d,errorRaised:e};throw e}}function w(t,e){e=e||B.languages||o(_);var r={relevance:0,value:x(t)},a=r;return e.filter(D).filter(L).forEach(function(e){var n=T(e,t,!1);n.language=e,n.relevance>a.relevance&&(a=n),n.relevance>r.relevance&&(a=r,r=n)}),a.language&&(r.second_best=a),r}function b(e){return B.tabReplace||B.useBR?e.replace(t,function(e,n){return B.useBR&&\"\\n\"===e?\"<br>\":B.tabReplace?n.replace(/\\t/g,B.tabReplace):\"\"}):e}function s(e){var n,t,r,a,i,o,c,l,u,s,f=function(e){var n,t,r,a,i=e.className+\" \";if(i+=e.parentNode?e.parentNode.className:\"\",t=E.exec(i)){var o=D(t[1]);return o||(console.warn(O.replace(\"{}\",t[1])),console.warn(\"Falling back to no-highlight mode for this block.\",e)),o?t[1]:\"no-highlight\"}for(n=0,r=(i=i.split(/\\s+/)).length;n<r;n++)if(R(a=i[n])||D(a))return a}(e);R(f)||(B.useBR?(n=document.createElement(\"div\")).innerHTML=e.innerHTML.replace(/\\n/g,\"\").replace(/<br[ \\/]*>/g,\"\\n\"):n=e,i=n.textContent,r=f?T(f,i,!0):w(i),(t=p(n)).length&&((a=document.createElement(\"div\")).innerHTML=r.value,r.value=v(t,p(a),i)),r.value=b(r.value),e.innerHTML=r.value,e.className=(o=e.className,c=f,l=r.language,u=c?g[c]:l,s=[o.trim()],o.match(/\\bhljs\\b/)||s.push(\"hljs\"),-1===o.indexOf(u)&&s.push(u),s.join(\" \").trim()),e.result={language:r.language,re:r.relevance},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.relevance}))}function h(){if(!h.called){h.called=!0;var e=document.querySelectorAll(\"pre code\");f.forEach.call(e,s)}}var N={disableAutodetect:!0};function D(e){return e=(e||\"\").toLowerCase(),_[e]||_[g[e]]}function L(e){var n=D(e);return n&&!n.disableAutodetect}return a.highlight=T,a.highlightAuto=w,a.fixMarkup=b,a.highlightBlock=s,a.configure=function(e){B=i(B,e)},a.initHighlighting=h,a.initHighlightingOnLoad=function(){window.addEventListener(\"DOMContentLoaded\",h,!1),window.addEventListener(\"load\",h,!1)},a.registerLanguage=function(n,e){var t;try{t=e(a)}catch(e){if(console.error(\"Language definition for '{}' could not be registered.\".replace(\"{}\",n)),!C)throw e;console.error(e),t=N}u(_[n]=t),t.rawDefinition=e.bind(null,a),t.aliases&&t.aliases.forEach(function(e){g[e]=n})},a.listLanguages=function(){return o(_)},a.getLanguage=D,a.requireLanguage=function(e){var n=D(e);if(n)return n;throw new Error(\"The '{}' language is required, but not loaded.\".replace(\"{}\",e))},a.autoDetection=L,a.inherit=i,a.debugMode=function(){C=!1},a.IR=a.IDENT_RE=\"[a-zA-Z]\\\\w*\",a.UIR=a.UNDERSCORE_IDENT_RE=\"[a-zA-Z_]\\\\w*\",a.NR=a.NUMBER_RE=\"\\\\b\\\\d+(\\\\.\\\\d+)?\",a.CNR=a.C_NUMBER_RE=\"(-?)(\\\\b0[xX][a-fA-F0-9]+|(\\\\b\\\\d+(\\\\.\\\\d*)?|\\\\.\\\\d+)([eE][-+]?\\\\d+)?)\",a.BNR=a.BINARY_NUMBER_RE=\"\\\\b(0b[01]+)\",a.RSR=a.RE_STARTERS_RE=\"!|!=|!==|%|%=|&|&&|&=|\\\\*|\\\\*=|\\\\+|\\\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\\\?|\\\\[|\\\\{|\\\\(|\\\\^|\\\\^=|\\\\||\\\\|=|\\\\|\\\\||~\",a.BE=a.BACKSLASH_ESCAPE={b:\"\\\\\\\\[\\\\s\\\\S]\",relevance:0},a.ASM=a.APOS_STRING_MODE={cN:\"string\",b:\"'\",e:\"'\",i:\"\\\\n\",c:[a.BE]},a.QSM=a.QUOTE_STRING_MODE={cN:\"string\",b:'\"',e:'\"',i:\"\\\\n\",c:[a.BE]},a.PWM=a.PHRASAL_WORDS_MODE={b:/\\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\\b/},a.C=a.COMMENT=function(e,n,t){var r=a.inherit({cN:\"comment\",b:e,e:n,c:[]},t||{});return r.c.push(a.PWM),r.c.push({cN:\"doctag\",b:\"(?:TODO|FIXME|NOTE|BUG|XXX):\",relevance:0}),r},a.CLCM=a.C_LINE_COMMENT_MODE=a.C(\"//\",\"$\"),a.CBCM=a.C_BLOCK_COMMENT_MODE=a.C(\"/\\\\*\",\"\\\\*/\"),a.HCM=a.HASH_COMMENT_MODE=a.C(\"#\",\"$\"),a.NM=a.NUMBER_MODE={cN:\"number\",b:a.NR,relevance:0},a.CNM=a.C_NUMBER_MODE={cN:\"number\",b:a.CNR,relevance:0},a.BNM=a.BINARY_NUMBER_MODE={cN:\"number\",b:a.BNR,relevance:0},a.CSSNM=a.CSS_NUMBER_MODE={cN:\"number\",b:a.NR+\"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?\",relevance:0},a.RM=a.REGEXP_MODE={cN:\"regexp\",b:/\\//,e:/\\/[gimuy]*/,i:/\\n/,c:[a.BE,{b:/\\[/,e:/\\]/,relevance:0,c:[a.BE]}]},a.TM=a.TITLE_MODE={cN:\"title\",b:a.IR,relevance:0},a.UTM=a.UNDERSCORE_TITLE_MODE={cN:\"title\",b:a.UIR,relevance:0},a.METHOD_GUARD={b:\"\\\\.\\\\s*\"+a.UIR,relevance:0},[a.BE,a.ASM,a.QSM,a.PWM,a.C,a.CLCM,a.CBCM,a.HCM,a.NM,a.CNM,a.BNM,a.CSSNM,a.RM,a.TM,a.UTM,a.METHOD_GUARD].forEach(function(e){!function n(t){Object.freeze(t);var r=\"function\"==typeof t;Object.getOwnPropertyNames(t).forEach(function(e){!t.hasOwnProperty(e)||null===t[e]||\"object\"!=typeof t[e]&&\"function\"!=typeof t[e]||r&&(\"caller\"===e||\"callee\"===e||\"arguments\"===e)||Object.isFrozen(t[e])||n(t[e])});return t}(e)}),a});hljs.registerLanguage(\"swift\",function(e){var i={keyword:\"#available #colorLiteral #column #else #elseif #endif #file #fileLiteral #function #if #imageLiteral #line #selector #sourceLocation _ __COLUMN__ __FILE__ __FUNCTION__ __LINE__ Any as as! as? associatedtype associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false fileprivate final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating open operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet\",literal:\"true false nil\",built_in:\"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip\"},t=e.C(\"/\\\\*\",\"\\\\*/\",{c:[\"self\"]}),n={cN:\"subst\",b:/\\\\\\(/,e:\"\\\\)\",k:i,c:[]},r={cN:\"string\",c:[e.BE,n],v:[{b:/\"\"\"/,e:/\"\"\"/},{b:/\"/,e:/\"/}]},a={cN:\"number\",b:\"\\\\b([\\\\d_]+(\\\\.[\\\\deE_]+)?|0x[a-fA-F0-9_]+(\\\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\\\b\",relevance:0};return n.c=[a],{k:i,c:[r,e.CLCM,t,{cN:\"type\",b:\"\\\\b[A-Z][\\\\wÀ-ʸ']*[!?]\"},{cN:\"type\",b:\"\\\\b[A-Z][\\\\wÀ-ʸ']*\",relevance:0},a,{cN:\"function\",bK:\"func\",e:\"{\",eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{b:/</,e:/>/},{cN:\"params\",b:/\\(/,e:/\\)/,endsParent:!0,k:i,c:[\"self\",a,r,e.CBCM,{b:\":\"}],i:/[\"']/}],i:/\\[|%/},{cN:\"class\",bK:\"struct protocol class extension enum\",k:i,e:\"\\\\{\",eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][\\u00C0-\\u02B80-9A-Za-z$_]*/})]},{cN:\"meta\",b:\"(@discardableResult|@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@objcMembers|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain|@dynamicMemberLookup|@propertyWrapper)\"},{bK:\"import\",e:/$/,c:[e.CLCM,t]}]}});hljs.registerLanguage(\"ini\",function(e){var b={cN:\"number\",relevance:0,v:[{b:/([\\+\\-]+)?[\\d]+_[\\d_]+/},{b:e.NR}]},a=e.C();a.v=[{b:/;/,e:/$/},{b:/#/,e:/$/}];var c={cN:\"variable\",v:[{b:/\\$[\\w\\d\"][\\w\\d_]*/},{b:/\\$\\{(.*?)}/}]},r={cN:\"literal\",b:/\\bon|off|true|false|yes|no\\b/},n={cN:\"string\",c:[e.BE],v:[{b:\"'''\",e:\"'''\",relevance:10},{b:'\"\"\"',e:'\"\"\"',relevance:10},{b:'\"',e:'\"'},{b:\"'\",e:\"'\"}]};return{aliases:[\"toml\"],cI:!0,i:/\\S/,c:[a,{cN:\"section\",b:/\\[+/,e:/\\]+/},{b:/^[a-z0-9\\[\\]_\\.-]+(?=\\s*=\\s*)/,cN:\"attr\",starts:{e:/$/,c:[a,{b:/\\[/,e:/\\]/,c:[a,r,c,n,b,\"self\"],relevance:0},r,c,n,b]}}]}});hljs.registerLanguage(\"python\",function(e){var r={keyword:\"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10\",built_in:\"Ellipsis NotImplemented\",literal:\"False None True\"},b={cN:\"meta\",b:/^(>>>|\\.\\.\\.) /},c={cN:\"subst\",b:/\\{/,e:/\\}/,k:r,i:/#/},a={b:/\\{\\{/,relevance:0},l={cN:\"string\",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[e.BE,b],relevance:10},{b:/(u|b)?r?\"\"\"/,e:/\"\"\"/,c:[e.BE,b],relevance:10},{b:/(fr|rf|f)'''/,e:/'''/,c:[e.BE,b,a,c]},{b:/(fr|rf|f)\"\"\"/,e:/\"\"\"/,c:[e.BE,b,a,c]},{b:/(u|r|ur)'/,e:/'/,relevance:10},{b:/(u|r|ur)\"/,e:/\"/,relevance:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)\"/,e:/\"/},{b:/(fr|rf|f)'/,e:/'/,c:[e.BE,a,c]},{b:/(fr|rf|f)\"/,e:/\"/,c:[e.BE,a,c]},e.ASM,e.QSM]},n={cN:\"number\",relevance:0,v:[{b:e.BNR+\"[lLjJ]?\"},{b:\"\\\\b(0o[0-7]+)[lLjJ]?\"},{b:e.CNR+\"[lLjJ]?\"}]},i={cN:\"params\",b:/\\(/,e:/\\)/,c:[\"self\",b,n,l,e.HCM]};return c.c=[l,n,b],{aliases:[\"py\",\"gyp\",\"ipython\"],k:r,i:/(<\\/|->|\\?)|=>/,c:[b,n,{bK:\"if\",relevance:0},l,e.HCM,{v:[{cN:\"function\",bK:\"def\"},{cN:\"class\",bK:\"class\"}],e:/:/,i:/[${=;\\n,]/,c:[e.UTM,i,{b:/->/,eW:!0,k:\"None\"}]},{cN:\"meta\",b:/^[\\t ]*@/,e:/$/},{b:/\\b(print|exec)\\(/}]}});hljs.registerLanguage(\"ruby\",function(e){var c=\"[a-zA-Z_]\\\\w*[!?=]?|[-+~]\\\\@|<<|>>|=~|===?|<=>|[<>]=?|\\\\*\\\\*|[-/+%^&*~`|]|\\\\[\\\\]=?\",b={keyword:\"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor\",literal:\"true false nil\"},r={cN:\"doctag\",b:\"@[A-Za-z]+\"},a={b:\"#<\",e:\">\"},n=[e.C(\"#\",\"$\",{c:[r]}),e.C(\"^\\\\=begin\",\"^\\\\=end\",{c:[r],relevance:10}),e.C(\"^__END__\",\"\\\\n$\")],s={cN:\"subst\",b:\"#\\\\{\",e:\"}\",k:b},t={cN:\"string\",c:[e.BE,s],v:[{b:/'/,e:/'/},{b:/\"/,e:/\"/},{b:/`/,e:/`/},{b:\"%[qQwWx]?\\\\(\",e:\"\\\\)\"},{b:\"%[qQwWx]?\\\\[\",e:\"\\\\]\"},{b:\"%[qQwWx]?{\",e:\"}\"},{b:\"%[qQwWx]?<\",e:\">\"},{b:\"%[qQwWx]?/\",e:\"/\"},{b:\"%[qQwWx]?%\",e:\"%\"},{b:\"%[qQwWx]?-\",e:\"-\"},{b:\"%[qQwWx]?\\\\|\",e:\"\\\\|\"},{b:/\\B\\?(\\\\\\d{1,3}|\\\\x[A-Fa-f0-9]{1,2}|\\\\u[A-Fa-f0-9]{4}|\\\\?\\S)\\b/},{b:/<<[-~]?'?(\\w+)(?:.|\\n)*?\\n\\s*\\1\\b/,rB:!0,c:[{b:/<<[-~]?'?/},{b:/\\w+/,endSameAsBegin:!0,c:[e.BE,s]}]}]},i={cN:\"params\",b:\"\\\\(\",e:\"\\\\)\",endsParent:!0,k:b},l=[t,a,{cN:\"class\",bK:\"class module\",e:\"$|;\",i:/=/,c:[e.inherit(e.TM,{b:\"[A-Za-z_]\\\\w*(::\\\\w+)*(\\\\?|\\\\!)?\"}),{b:\"<\\\\s*\",c:[{b:\"(\"+e.IR+\"::)?\"+e.IR}]}].concat(n)},{cN:\"function\",bK:\"def\",e:\"$|;\",c:[e.inherit(e.TM,{b:c}),i].concat(n)},{b:e.IR+\"::\"},{cN:\"symbol\",b:e.UIR+\"(\\\\!|\\\\?)?:\",relevance:0},{cN:\"symbol\",b:\":(?!\\\\s)\",c:[t,{b:c}],relevance:0},{cN:\"number\",b:\"(\\\\b0[0-7_]+)|(\\\\b0x[0-9a-fA-F_]+)|(\\\\b[1-9][0-9_]*(\\\\.[0-9_]+)?)|[0_]\\\\b\",relevance:0},{b:\"(\\\\$\\\\W)|((\\\\$|\\\\@\\\\@?)(\\\\w+))\"},{cN:\"params\",b:/\\|/,e:/\\|/,k:b},{b:\"(\"+e.RSR+\"|unless)\\\\s*\",k:\"unless\",c:[a,{cN:\"regexp\",c:[e.BE,s],i:/\\n/,v:[{b:\"/\",e:\"/[a-z]*\"},{b:\"%r{\",e:\"}[a-z]*\"},{b:\"%r\\\\(\",e:\"\\\\)[a-z]*\"},{b:\"%r!\",e:\"![a-z]*\"},{b:\"%r\\\\[\",e:\"\\\\][a-z]*\"}]}].concat(n),relevance:0}].concat(n);s.c=l;var d=[{b:/^\\s*=>/,starts:{e:\"$\",c:i.c=l}},{cN:\"meta\",b:\"^([>?]>|[\\\\w#]+\\\\(\\\\w+\\\\):\\\\d+:\\\\d+>|(\\\\w+-)?\\\\d+\\\\.\\\\d+\\\\.\\\\d(p\\\\d+)?[^>]+>)\",starts:{e:\"$\",c:l}}];return{aliases:[\"rb\",\"gemspec\",\"podspec\",\"thor\",\"irb\"],k:b,i:/\\/\\*/,c:n.concat(d).concat(l)}});hljs.registerLanguage(\"yaml\",function(e){var b=\"true false yes no null\",a={cN:\"string\",relevance:0,v:[{b:/'/,e:/'/},{b:/\"/,e:/\"/},{b:/\\S+/}],c:[e.BE,{cN:\"template-variable\",v:[{b:\"{{\",e:\"}}\"},{b:\"%{\",e:\"}\"}]}]};return{cI:!0,aliases:[\"yml\",\"YAML\",\"yaml\"],c:[{cN:\"attr\",v:[{b:\"\\\\w[\\\\w :\\\\/.-]*:(?=[ \\t]|$)\"},{b:'\"\\\\w[\\\\w :\\\\/.-]*\":(?=[ \\t]|$)'},{b:\"'\\\\w[\\\\w :\\\\/.-]*':(?=[ \\t]|$)\"}]},{cN:\"meta\",b:\"^---s*$\",relevance:10},{cN:\"string\",b:\"[\\\\|>]([0-9]?[+-])?[ ]*\\\\n( *)[\\\\S ]+\\\\n(\\\\2[\\\\S ]+\\\\n?)*\"},{b:\"<%[%=-]?\",e:\"[%-]?%>\",sL:\"ruby\",eB:!0,eE:!0,relevance:0},{cN:\"type\",b:\"!\"+e.UIR},{cN:\"type\",b:\"!!\"+e.UIR},{cN:\"meta\",b:\"&\"+e.UIR+\"$\"},{cN:\"meta\",b:\"\\\\*\"+e.UIR+\"$\"},{cN:\"bullet\",b:\"\\\\-(?=[ ]|$)\",relevance:0},e.HCM,{bK:b,k:{literal:b}},{cN:\"number\",b:e.CNR+\"\\\\b\"},a]}});hljs.registerLanguage(\"coffeescript\",function(e){var c={keyword:\"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super yield import export from as default await then unless until loop of by when and or is isnt not\",literal:\"true false null undefined yes no on off\",built_in:\"npm require console print module global window document\"},n=\"[A-Za-z$_][0-9A-Za-z$_]*\",r={cN:\"subst\",b:/#\\{/,e:/}/,k:c},i=[e.BNM,e.inherit(e.CNM,{starts:{e:\"(\\\\s*/)?\",relevance:0}}),{cN:\"string\",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/\"\"\"/,e:/\"\"\"/,c:[e.BE,r]},{b:/\"/,e:/\"/,c:[e.BE,r]}]},{cN:\"regexp\",v:[{b:\"///\",e:\"///\",c:[r,e.HCM]},{b:\"//[gim]{0,3}(?=\\\\W)\",relevance:0},{b:/\\/(?![ *]).*?(?![\\\\]).\\/[gim]{0,3}(?=\\W)/}]},{b:\"@\"+n},{sL:\"javascript\",eB:!0,eE:!0,v:[{b:\"```\",e:\"```\"},{b:\"`\",e:\"`\"}]}];r.c=i;var s=e.inherit(e.TM,{b:n}),t=\"(\\\\(.*\\\\))?\\\\s*\\\\B[-=]>\",a={cN:\"params\",b:\"\\\\([^\\\\(]\",rB:!0,c:[{b:/\\(/,e:/\\)/,k:c,c:[\"self\"].concat(i)}]};return{aliases:[\"coffee\",\"cson\",\"iced\"],k:c,i:/\\/\\*/,c:i.concat([e.C(\"###\",\"###\"),e.HCM,{cN:\"function\",b:\"^\\\\s*\"+n+\"\\\\s*=\\\\s*\"+t,e:\"[-=]>\",rB:!0,c:[s,a]},{b:/[:\\(,=]\\s*/,relevance:0,c:[{cN:\"function\",b:t,e:\"[-=]>\",rB:!0,c:[a]}]},{cN:\"class\",bK:\"class\",e:\"$\",i:/[:=\"\\[\\]]/,c:[{bK:\"extends\",eW:!0,i:/[:=\"\\[\\]]/,c:[s]},s]},{b:n+\":\",e:\":\",rB:!0,rE:!0,relevance:0}])}});hljs.registerLanguage(\"rust\",function(e){var t=\"([ui](8|16|32|64|128|size)|f(32|64))?\",r=\"drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!\";return{aliases:[\"rs\"],k:{keyword:\"abstract as async await become box break const continue crate do dyn else enum extern false final fn for if impl in let loop macro match mod move mut override priv pub ref return self Self static struct super trait true try type typeof unsafe unsized use virtual where while yield\",literal:\"true false Some None Ok Err\",built_in:r},l:e.IR+\"!?\",i:\"</\",c:[e.CLCM,e.C(\"/\\\\*\",\"\\\\*/\",{c:[\"self\"]}),e.inherit(e.QSM,{b:/b?\"/,i:null}),{cN:\"string\",v:[{b:/r(#*)\"(.|\\n)*?\"\\1(?!#)/},{b:/b?'\\\\?(x\\w{2}|u\\w{4}|U\\w{8}|.)'/}]},{cN:\"symbol\",b:/'[a-zA-Z_][a-zA-Z0-9_]*/},{cN:\"number\",v:[{b:\"\\\\b0b([01_]+)\"+t},{b:\"\\\\b0o([0-7_]+)\"+t},{b:\"\\\\b0x([A-Fa-f0-9_]+)\"+t},{b:\"\\\\b(\\\\d[\\\\d_]*(\\\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)\"+t}],relevance:0},{cN:\"function\",bK:\"fn\",e:\"(\\\\(|<)\",eE:!0,c:[e.UTM]},{cN:\"meta\",b:\"#\\\\!?\\\\[\",e:\"\\\\]\",c:[{cN:\"meta-string\",b:/\"/,e:/\"/}]},{cN:\"class\",bK:\"type\",e:\";\",c:[e.inherit(e.UTM,{endsParent:!0})],i:\"\\\\S\"},{cN:\"class\",bK:\"trait enum struct union\",e:\"{\",c:[e.inherit(e.UTM,{endsParent:!0})],i:\"[\\\\w\\\\d]\"},{b:e.IR+\"::\",k:{built_in:r}},{b:\"->\"}]}});hljs.registerLanguage(\"cpp\",function(e){function t(e){return\"(?:\"+e+\")?\"}var r=\"decltype\\\\(auto\\\\)\",a=\"[a-zA-Z_]\\\\w*::\",i=(t(a),t(\"<.*?>\"),{cN:\"keyword\",b:\"\\\\b[a-z\\\\d_]*_t\\\\b\"}),c={cN:\"string\",v:[{b:'(u8?|U|L)?\"',e:'\"',i:\"\\\\n\",c:[e.BE]},{b:\"(u8?|U|L)?'(\\\\\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\\\S)|.)\",e:\"'\",i:\".\"},{b:/(?:u8?|U|L)?R\"([^()\\\\ ]{0,16})\\((?:.|\\n)*?\\)\\1\"/}]},s={cN:\"number\",v:[{b:\"\\\\b(0b[01']+)\"},{b:\"(-?)\\\\b([\\\\d']+(\\\\.[\\\\d']*)?|\\\\.[\\\\d']+)(u|U|l|L|ul|UL|f|F|b|B)\"},{b:\"(-?)(\\\\b0[xX][a-fA-F0-9']+|(\\\\b[\\\\d']+(\\\\.[\\\\d']*)?|\\\\.[\\\\d']+)([eE][-+]?[\\\\d']+)?)\"}],relevance:0},n={cN:\"meta\",b:/#\\s*[a-z]+\\b/,e:/$/,k:{\"meta-keyword\":\"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include\"},c:[{b:/\\\\\\n/,relevance:0},e.inherit(c,{cN:\"meta-string\"}),{cN:\"meta-string\",b:/<.*?>/,e:/$/,i:\"\\\\n\"},e.CLCM,e.CBCM]},o={cN:\"title\",b:t(a)+e.IR,relevance:0},l=t(a)+e.IR+\"\\\\s*\\\\(\",u={keyword:\"int float while private char char8_t char16_t char32_t catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid wchar_tshort reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignas alignof constexpr consteval constinit decltype concept co_await co_return co_yield requires noexcept static_assert thread_local restrict final override atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and and_eq bitand bitor compl not not_eq or or_eq xor xor_eq\",built_in:\"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr _Bool complex _Complex imaginary _Imaginary\",literal:\"true false nullptr NULL\"},p=[i,e.CLCM,e.CBCM,s,c],m={v:[{b:/=/,e:/;/},{b:/\\(/,e:/\\)/},{bK:\"new throw return else\",e:/;/}],k:u,c:p.concat([{b:/\\(/,e:/\\)/,k:u,c:p.concat([\"self\"]),relevance:0}]),relevance:0},d={cN:\"function\",b:\"((decltype\\\\(auto\\\\)|(?:[a-zA-Z_]\\\\w*::)?[a-zA-Z_]\\\\w*(?:<.*?>)?)[\\\\*&\\\\s]+)+\"+l,rB:!0,e:/[{;=]/,eE:!0,k:u,i:/[^\\w\\s\\*&:<>]/,c:[{b:r,k:u,relevance:0},{b:l,rB:!0,c:[o],relevance:0},{cN:\"params\",b:/\\(/,e:/\\)/,k:u,relevance:0,c:[e.CLCM,e.CBCM,c,s,i,{b:/\\(/,e:/\\)/,k:u,relevance:0,c:[\"self\",e.CLCM,e.CBCM,c,s,i]}]},i,e.CLCM,e.CBCM,n]};return{aliases:[\"c\",\"cc\",\"h\",\"c++\",\"h++\",\"hpp\",\"hh\",\"hxx\",\"cxx\"],k:u,i:\"</\",c:[].concat(m,d,p,[n,{b:\"\\\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\\\s*<\",e:\">\",k:u,c:[\"self\",i]},{b:e.IR+\"::\",k:u},{cN:\"class\",bK:\"class struct\",e:/[{;:]/,c:[{b:/</,e:/>/,c:[\"self\"]},e.TM]}]),exports:{preprocessor:n,strings:c,k:u}}});hljs.registerLanguage(\"perl\",function(e){var t=\"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when\",r={cN:\"subst\",b:\"[$@]\\\\{\",e:\"\\\\}\",k:t},s={b:\"->{\",e:\"}\"},n={v:[{b:/\\$\\d/},{b:/[\\$%@](\\^\\w\\b|#\\w+(::\\w+)*|{\\w+}|\\w+(::\\w*)*)/},{b:/[\\$%@][^\\s\\w{]/,relevance:0}]},c=[e.BE,r,n],a=[n,e.HCM,e.C(\"^\\\\=\\\\w\",\"\\\\=cut\",{eW:!0}),s,{cN:\"string\",c:c,v:[{b:\"q[qwxr]?\\\\s*\\\\(\",e:\"\\\\)\",relevance:5},{b:\"q[qwxr]?\\\\s*\\\\[\",e:\"\\\\]\",relevance:5},{b:\"q[qwxr]?\\\\s*\\\\{\",e:\"\\\\}\",relevance:5},{b:\"q[qwxr]?\\\\s*\\\\|\",e:\"\\\\|\",relevance:5},{b:\"q[qwxr]?\\\\s*\\\\<\",e:\"\\\\>\",relevance:5},{b:\"qw\\\\s+q\",e:\"q\",relevance:5},{b:\"'\",e:\"'\",c:[e.BE]},{b:'\"',e:'\"'},{b:\"`\",e:\"`\",c:[e.BE]},{b:\"{\\\\w+}\",c:[],relevance:0},{b:\"-?\\\\w+\\\\s*\\\\=\\\\>\",c:[],relevance:0}]},{cN:\"number\",b:\"(\\\\b0[0-7_]+)|(\\\\b0x[0-9a-fA-F_]+)|(\\\\b[1-9][0-9_]*(\\\\.[0-9_]+)?)|[0_]\\\\b\",relevance:0},{b:\"(\\\\/\\\\/|\"+e.RSR+\"|\\\\b(split|return|print|reverse|grep)\\\\b)\\\\s*\",k:\"split return print reverse grep\",relevance:0,c:[e.HCM,{cN:\"regexp\",b:\"(s|tr|y)/(\\\\\\\\.|[^/])*/(\\\\\\\\.|[^/])*/[a-z]*\",relevance:10},{cN:\"regexp\",b:\"(m|qr)?/\",e:\"/[a-z]*\",c:[e.BE],relevance:0}]},{cN:\"function\",bK:\"sub\",e:\"(\\\\s*\\\\(.*?\\\\))?[;{]\",eE:!0,relevance:5,c:[e.TM]},{b:\"-\\\\w\\\\b\",relevance:0},{b:\"^__DATA__$\",e:\"^__END__$\",sL:\"mojolicious\",c:[{b:\"^@@.*\",e:\"$\",cN:\"comment\"}]}];return r.c=a,{aliases:[\"pl\",\"pm\"],l:/[\\w\\.]+/,k:t,c:s.c=a}});hljs.registerLanguage(\"scss\",function(e){var t=\"@[a-z-]+\",r={cN:\"variable\",b:\"(\\\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\\\b\"},i={cN:\"number\",b:\"#[0-9A-Fa-f]+\"};e.CSSNM,e.QSM,e.ASM,e.CBCM;return{cI:!0,i:\"[=/|']\",c:[e.CLCM,e.CBCM,{cN:\"selector-id\",b:\"\\\\#[A-Za-z0-9_-]+\",relevance:0},{cN:\"selector-class\",b:\"\\\\.[A-Za-z0-9_-]+\",relevance:0},{cN:\"selector-attr\",b:\"\\\\[\",e:\"\\\\]\",i:\"$\"},{cN:\"selector-tag\",b:\"\\\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\\\b\",relevance:0},{cN:\"selector-pseudo\",b:\":(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)\"},{cN:\"selector-pseudo\",b:\"::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)\"},r,{cN:\"attribute\",b:\"\\\\b(src|z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\\\b\",i:\"[^\\\\s]\"},{b:\"\\\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\\\b\"},{b:\":\",e:\";\",c:[r,i,e.CSSNM,e.QSM,e.ASM,{cN:\"meta\",b:\"!important\"}]},{b:\"@(page|font-face)\",l:t,k:\"@page @font-face\"},{b:\"@\",e:\"[{;]\",rB:!0,k:\"and or not only\",c:[{b:t,cN:\"keyword\"},r,e.QSM,e.ASM,i,e.CSSNM]}]}});hljs.registerLanguage(\"apache\",function(e){var r={cN:\"number\",b:\"[\\\\$%]\\\\d+\"};return{aliases:[\"apacheconf\"],cI:!0,c:[e.HCM,{cN:\"section\",b:\"</?\",e:\">\"},{cN:\"attribute\",b:/\\w+/,relevance:0,k:{nomarkup:\"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername\"},starts:{e:/$/,relevance:0,k:{literal:\"on off all\"},c:[{cN:\"meta\",b:\"\\\\s\\\\[\",e:\"\\\\]$\"},{cN:\"variable\",b:\"[\\\\$%]\\\\{\",e:\"\\\\}\",c:[\"self\",r]},r,e.QSM]}}],i:/\\S/}});hljs.registerLanguage(\"typescript\",function(e){var r=\"[A-Za-z$_][0-9A-Za-z$_]*\",t={keyword:\"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class public private protected get set super static implements enum export import declare type namespace abstract as from extends async await\",literal:\"true false null undefined NaN Infinity\",built_in:\"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void Promise\"},n={cN:\"meta\",b:\"@\"+r},a={b:\"\\\\(\",e:/\\)/,k:t,c:[\"self\",e.QSM,e.ASM,e.NM]},c={cN:\"params\",b:/\\(/,e:/\\)/,eB:!0,eE:!0,k:t,c:[e.CLCM,e.CBCM,n,a]},s={cN:\"number\",v:[{b:\"\\\\b(0[bB][01]+)n?\"},{b:\"\\\\b(0[oO][0-7]+)n?\"},{b:e.CNR+\"n?\"}],relevance:0},o={cN:\"subst\",b:\"\\\\$\\\\{\",e:\"\\\\}\",k:t,c:[]},i={b:\"html`\",e:\"\",starts:{e:\"`\",rE:!1,c:[e.BE,o],sL:\"xml\"}},l={b:\"css`\",e:\"\",starts:{e:\"`\",rE:!1,c:[e.BE,o],sL:\"css\"}},b={cN:\"string\",b:\"`\",e:\"`\",c:[e.BE,o]};return o.c=[e.ASM,e.QSM,i,l,b,s,e.RM],{aliases:[\"ts\"],k:t,c:[{cN:\"meta\",b:/^\\s*['\"]use strict['\"]/},e.ASM,e.QSM,i,l,b,e.CLCM,e.CBCM,s,{b:\"(\"+e.RSR+\"|\\\\b(case|return|throw)\\\\b)\\\\s*\",k:\"return throw case\",c:[e.CLCM,e.CBCM,e.RM,{cN:\"function\",b:\"(\\\\(.*?\\\\)|\"+e.IR+\")\\\\s*=>\",rB:!0,e:\"\\\\s*=>\",c:[{cN:\"params\",v:[{b:e.IR},{b:/\\(\\s*\\)/},{b:/\\(/,e:/\\)/,eB:!0,eE:!0,k:t,c:[\"self\",e.CLCM,e.CBCM]}]}]}],relevance:0},{cN:\"function\",bK:\"function\",e:/[\\{;]/,eE:!0,k:t,c:[\"self\",e.inherit(e.TM,{b:r}),c],i:/%/,relevance:0},{bK:\"constructor\",e:/[\\{;]/,eE:!0,c:[\"self\",c]},{b:/module\\./,k:{built_in:\"module\"},relevance:0},{bK:\"module\",e:/\\{/,eE:!0},{bK:\"interface\",e:/\\{/,eE:!0,k:\"interface extends\"},{b:/\\$[(.]/},{b:\"\\\\.\"+e.IR,relevance:0},n,a]}});hljs.registerLanguage(\"bash\",function(e){var t={cN:\"variable\",v:[{b:/\\$[\\w\\d#@][\\w\\d_]*/},{b:/\\$\\{(.*?)}/}]},a={cN:\"string\",b:/\"/,e:/\"/,c:[e.BE,t,{cN:\"variable\",b:/\\$\\(/,e:/\\)/,c:[e.BE]}]};return{aliases:[\"sh\",\"zsh\"],l:/\\b-?[a-z\\._]+\\b/,k:{keyword:\"if then else elif fi for while in do done case esac function\",literal:\"true false\",built_in:\"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp\",_:\"-ne -eq -lt -gt -f -d -e -s -l -a\"},c:[{cN:\"meta\",b:/^#![^\\n]+sh\\s*$/,relevance:10},{cN:\"function\",b:/\\w[\\w\\d_]*\\s*\\(\\s*\\)\\s*\\{/,rB:!0,c:[e.inherit(e.TM,{b:/\\w[\\w\\d_]*/})],relevance:0},e.HCM,a,{cN:\"\",b:/\\\\\"/},{cN:\"string\",b:/'/,e:/'/},t]}});hljs.registerLanguage(\"shell\",function(s){return{aliases:[\"console\"],c:[{cN:\"meta\",b:\"^\\\\s{0,3}[/\\\\w\\\\d\\\\[\\\\]()@-]*[>%$#]\",starts:{e:\"$\",sL:\"bash\"}}]}});hljs.registerLanguage(\"go\",function(e){var n={keyword:\"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune\",literal:\"true false iota nil\",built_in:\"append cap close complex copy imag len make new panic print println real recover delete\"};return{aliases:[\"golang\"],k:n,i:\"</\",c:[e.CLCM,e.CBCM,{cN:\"string\",v:[e.QSM,e.ASM,{b:\"`\",e:\"`\"}]},{cN:\"number\",v:[{b:e.CNR+\"[i]\",relevance:1},e.CNM]},{b:/:=/},{cN:\"function\",bK:\"func\",e:\"\\\\s*(\\\\{|$)\",eE:!0,c:[e.TM,{cN:\"params\",b:/\\(/,e:/\\)/,k:n,i:/[\"']/}]}]}});hljs.registerLanguage(\"nginx\",function(e){var r={cN:\"variable\",v:[{b:/\\$\\d+/},{b:/\\$\\{/,e:/}/},{b:\"[\\\\$\\\\@]\"+e.UIR}]},b={eW:!0,l:\"[a-z/_]+\",k:{literal:\"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll\"},relevance:0,i:\"=>\",c:[e.HCM,{cN:\"string\",c:[e.BE,r],v:[{b:/\"/,e:/\"/},{b:/'/,e:/'/}]},{b:\"([a-z]+):/\",e:\"\\\\s\",eW:!0,eE:!0,c:[r]},{cN:\"regexp\",c:[e.BE,r],v:[{b:\"\\\\s\\\\^\",e:\"\\\\s|{|;\",rE:!0},{b:\"~\\\\*?\\\\s+\",e:\"\\\\s|{|;\",rE:!0},{b:\"\\\\*(\\\\.[a-z\\\\-]+)+\"},{b:\"([a-z\\\\-]+\\\\.)+\\\\*\"}]},{cN:\"number\",b:\"\\\\b\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}(:\\\\d{1,5})?\\\\b\"},{cN:\"number\",b:\"\\\\b\\\\d+[kKmMgGdshdwy]*\\\\b\",relevance:0},r]};return{aliases:[\"nginxconf\"],c:[e.HCM,{b:e.UIR+\"\\\\s+{\",rB:!0,e:\"{\",c:[{cN:\"section\",b:e.UIR}],relevance:0},{b:e.UIR+\"\\\\s\",e:\";|{\",rB:!0,c:[{cN:\"attribute\",b:e.UIR,starts:b}],relevance:0}],i:\"[^\\\\s\\\\}]\"}});hljs.registerLanguage(\"java\",function(e){var a=\"false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do\",t={cN:\"number\",b:\"\\\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\\\d]+[\\\\d_]+[\\\\d]+|[\\\\d]+)(\\\\.([\\\\d]+[\\\\d_]+[\\\\d]+|[\\\\d]+))?|\\\\.([\\\\d]+[\\\\d_]+[\\\\d]+|[\\\\d]+))([eE][-+]?\\\\d+)?)[lLfF]?\",relevance:0};return{aliases:[\"jsp\"],k:a,i:/<\\/|#/,c:[e.C(\"/\\\\*\\\\*\",\"\\\\*/\",{relevance:0,c:[{b:/\\w+@/,relevance:0},{cN:\"doctag\",b:\"@[A-Za-z]+\"}]}),e.CLCM,e.CBCM,e.ASM,e.QSM,{cN:\"class\",bK:\"class interface\",e:/[{;=]/,eE:!0,k:\"class interface\",i:/[:\"\\[\\]]/,c:[{bK:\"extends implements\"},e.UTM]},{bK:\"new throw return else\",relevance:0},{cN:\"function\",b:\"([À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(<[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(\\\\s*,\\\\s*[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*)*>)?\\\\s+)+\"+e.UIR+\"\\\\s*\\\\(\",rB:!0,e:/[{;=]/,eE:!0,k:a,c:[{b:e.UIR+\"\\\\s*\\\\(\",rB:!0,relevance:0,c:[e.UTM]},{cN:\"params\",b:/\\(/,e:/\\)/,k:a,relevance:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]},t,{cN:\"meta\",b:\"@[A-Za-z]+\"}]}});hljs.registerLanguage(\"xml\",function(e){var c={cN:\"symbol\",b:\"&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;\"},s={b:\"\\\\s\",c:[{cN:\"meta-keyword\",b:\"#?[a-z_][a-z1-9_-]+\",i:\"\\\\n\"}]},a=e.inherit(s,{b:\"\\\\(\",e:\"\\\\)\"}),t=e.inherit(e.ASM,{cN:\"meta-string\"}),l=e.inherit(e.QSM,{cN:\"meta-string\"}),r={eW:!0,i:/</,relevance:0,c:[{cN:\"attr\",b:\"[A-Za-z0-9\\\\._:-]+\",relevance:0},{b:/=\\s*/,relevance:0,c:[{cN:\"string\",endsParent:!0,v:[{b:/\"/,e:/\"/,c:[c]},{b:/'/,e:/'/,c:[c]},{b:/[^\\s\"'=<>`]+/}]}]}]};return{aliases:[\"html\",\"xhtml\",\"rss\",\"atom\",\"xjb\",\"xsd\",\"xsl\",\"plist\",\"wsf\",\"svg\"],cI:!0,c:[{cN:\"meta\",b:\"<![a-z]\",e:\">\",relevance:10,c:[s,l,t,a,{b:\"\\\\[\",e:\"\\\\]\",c:[{cN:\"meta\",b:\"<![a-z]\",e:\">\",c:[s,a,l,t]}]}]},e.C(\"\\x3c!--\",\"--\\x3e\",{relevance:10}),{b:\"<\\\\!\\\\[CDATA\\\\[\",e:\"\\\\]\\\\]>\",relevance:10},c,{cN:\"meta\",b:/<\\?xml/,e:/\\?>/,relevance:10},{b:/<\\?(php)?/,e:/\\?>/,sL:\"php\",c:[{b:\"/\\\\*\",e:\"\\\\*/\",skip:!0},{b:'b\"',e:'\"',skip:!0},{b:\"b'\",e:\"'\",skip:!0},e.inherit(e.ASM,{i:null,cN:null,c:null,skip:!0}),e.inherit(e.QSM,{i:null,cN:null,c:null,skip:!0})]},{cN:\"tag\",b:\"<style(?=\\\\s|>)\",e:\">\",k:{name:\"style\"},c:[r],starts:{e:\"</style>\",rE:!0,sL:[\"css\",\"xml\"]}},{cN:\"tag\",b:\"<script(?=\\\\s|>)\",e:\">\",k:{name:\"script\"},c:[r],starts:{e:\"<\\/script>\",rE:!0,sL:[\"actionscript\",\"javascript\",\"handlebars\",\"xml\"]}},{cN:\"tag\",b:\"</?\",e:\"/?>\",c:[{cN:\"name\",b:/[^\\/><\\s]+/,relevance:0},r]}]}});hljs.registerLanguage(\"markdown\",function(e){return{aliases:[\"md\",\"mkdown\",\"mkd\"],c:[{cN:\"section\",v:[{b:\"^#{1,6}\",e:\"$\"},{b:\"^.+?\\\\n[=-]{2,}$\"}]},{b:\"<\",e:\">\",sL:\"xml\",relevance:0},{cN:\"bullet\",b:\"^\\\\s*([*+-]|(\\\\d+\\\\.))\\\\s+\"},{cN:\"strong\",b:\"[*_]{2}.+?[*_]{2}\"},{cN:\"emphasis\",v:[{b:\"\\\\*.+?\\\\*\"},{b:\"_.+?_\",relevance:0}]},{cN:\"quote\",b:\"^>\\\\s+\",e:\"$\"},{cN:\"code\",v:[{b:\"^```\\\\w*\\\\s*$\",e:\"^```[ ]*$\"},{b:\"`.+?`\"},{b:\"^( {4}|\\\\t)\",e:\"$\",relevance:0}]},{b:\"^[-\\\\*]{3,}\",e:\"$\"},{b:\"\\\\[.+?\\\\][\\\\(\\\\[].*?[\\\\)\\\\]]\",rB:!0,c:[{cN:\"string\",b:\"\\\\[\",e:\"\\\\]\",eB:!0,rE:!0,relevance:0},{cN:\"link\",b:\"\\\\]\\\\(\",e:\"\\\\)\",eB:!0,eE:!0},{cN:\"symbol\",b:\"\\\\]\\\\[\",e:\"\\\\]\",eB:!0,eE:!0}],relevance:10},{b:/^\\[[^\\n]+\\]:/,rB:!0,c:[{cN:\"symbol\",b:/\\[/,e:/\\]/,eB:!0,eE:!0},{cN:\"link\",b:/:\\s*/,e:/$/,eB:!0}]}]}});hljs.registerLanguage(\"less\",function(e){function r(e){return{cN:\"string\",b:\"~?\"+e+\".*?\"+e}}function t(e,r,t){return{cN:e,b:r,relevance:t}}var a=\"[\\\\w-]+\",c=\"(\"+a+\"|@{\"+a+\"})\",s=[],n=[],b={b:\"\\\\(\",e:\"\\\\)\",c:n,relevance:0};n.push(e.CLCM,e.CBCM,r(\"'\"),r('\"'),e.CSSNM,{b:\"(url|data-uri)\\\\(\",starts:{cN:\"string\",e:\"[\\\\)\\\\n]\",eE:!0}},t(\"number\",\"#[0-9A-Fa-f]+\\\\b\"),b,t(\"variable\",\"@@?\"+a,10),t(\"variable\",\"@{\"+a+\"}\"),t(\"built_in\",\"~?`[^`]*?`\"),{cN:\"attribute\",b:a+\"\\\\s*:\",e:\":\",rB:!0,eE:!0},{cN:\"meta\",b:\"!important\"});var i=n.concat({b:\"{\",e:\"}\",c:s}),l={bK:\"when\",eW:!0,c:[{bK:\"and not\"}].concat(n)},o={b:c+\"\\\\s*:\",rB:!0,e:\"[;}]\",relevance:0,c:[{cN:\"attribute\",b:c,e:\":\",eE:!0,starts:{eW:!0,i:\"[<=$]\",relevance:0,c:n}}]},u={cN:\"keyword\",b:\"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\\\b\",starts:{e:\"[;{}]\",rE:!0,c:n,relevance:0}},v={cN:\"variable\",v:[{b:\"@\"+a+\"\\\\s*:\",relevance:15},{b:\"@\"+a}],starts:{e:\"[;}]\",rE:!0,c:i}},C={v:[{b:\"[\\\\.#:&\\\\[>]\",e:\"[;{}]\"},{b:c,e:\"{\"}],rB:!0,rE:!0,i:\"[<='$\\\"]\",relevance:0,c:[e.CLCM,e.CBCM,l,t(\"keyword\",\"all\\\\b\"),t(\"variable\",\"@{\"+a+\"}\"),t(\"selector-tag\",c+\"%?\",0),t(\"selector-id\",\"#\"+c),t(\"selector-class\",\"\\\\.\"+c,0),t(\"selector-tag\",\"&\",0),{cN:\"selector-attr\",b:\"\\\\[\",e:\"\\\\]\"},{cN:\"selector-pseudo\",b:/:(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\"'.]+/},{b:\"\\\\(\",e:\"\\\\)\",c:i},{b:\"!important\"}]};return s.push(e.CLCM,e.CBCM,u,v,o,C),{cI:!0,i:\"[=>'/<($\\\"]\",c:s}});hljs.registerLanguage(\"properties\",function(e){var r=\"[ \\\\t\\\\f]*\",t=\"(\"+r+\"[:=]\"+r+\"|[ \\\\t\\\\f]+)\",n=\"([^\\\\\\\\\\\\W:= \\\\t\\\\f\\\\n]|\\\\\\\\.)+\",a=\"([^\\\\\\\\:= \\\\t\\\\f\\\\n]|\\\\\\\\.)+\",c={e:t,relevance:0,starts:{cN:\"string\",e:/$/,relevance:0,c:[{b:\"\\\\\\\\\\\\n\"}]}};return{cI:!0,i:/\\S/,c:[e.C(\"^\\\\s*[!#]\",\"$\"),{b:n+t,rB:!0,c:[{cN:\"attr\",b:n,endsParent:!0,relevance:0}],starts:c},{b:a+t,rB:!0,relevance:0,c:[{cN:\"meta\",b:a,endsParent:!0,relevance:0}],starts:c},{cN:\"attr\",relevance:0,b:a+r+\"$\"}]}});hljs.registerLanguage(\"lua\",function(e){var t=\"\\\\[=*\\\\[\",a=\"\\\\]=*\\\\]\",n={b:t,e:a,c:[\"self\"]},l=[e.C(\"--(?!\"+t+\")\",\"$\"),e.C(\"--\"+t,a,{c:[n],relevance:10})];return{l:e.UIR,k:{literal:\"true false nil\",keyword:\"and break do else elseif end for goto if in local not or repeat return then until while\",built_in:\"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstringmodule next pairs pcall print rawequal rawget rawset require select setfenvsetmetatable tonumber tostring type unpack xpcall arg selfcoroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove\"},c:l.concat([{cN:\"function\",bK:\"function\",e:\"\\\\)\",c:[e.inherit(e.TM,{b:\"([_a-zA-Z]\\\\w*\\\\.)*([_a-zA-Z]\\\\w*:)?[_a-zA-Z]\\\\w*\"}),{cN:\"params\",b:\"\\\\(\",eW:!0,c:l}].concat(l)},e.CNM,e.ASM,e.QSM,{cN:\"string\",b:t,e:a,c:[n],relevance:5}])}});hljs.registerLanguage(\"php\",function(e){var c={b:\"\\\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*\"},i={cN:\"meta\",b:/<\\?(php)?|\\?>/},t={cN:\"string\",c:[e.BE,i],v:[{b:'b\"',e:'\"'},{b:\"b'\",e:\"'\"},e.inherit(e.ASM,{i:null}),e.inherit(e.QSM,{i:null})]},a={v:[e.BNM,e.CNM]};return{aliases:[\"php\",\"php3\",\"php4\",\"php5\",\"php6\",\"php7\"],cI:!0,k:\"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally\",c:[e.HCM,e.C(\"//\",\"$\",{c:[i]}),e.C(\"/\\\\*\",\"\\\\*/\",{c:[{cN:\"doctag\",b:\"@[A-Za-z]+\"}]}),e.C(\"__halt_compiler.+?;\",!1,{eW:!0,k:\"__halt_compiler\",l:e.UIR}),{cN:\"string\",b:/<<<['\"]?\\w+['\"]?$/,e:/^\\w+;?$/,c:[e.BE,{cN:\"subst\",v:[{b:/\\$\\w+/},{b:/\\{\\$/,e:/\\}/}]}]},i,{cN:\"keyword\",b:/\\$this\\b/},c,{b:/(::|->)+[a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*/},{cN:\"function\",bK:\"function\",e:/[;{]/,eE:!0,i:\"\\\\$|\\\\[|%\",c:[e.UTM,{cN:\"params\",b:\"\\\\(\",e:\"\\\\)\",c:[\"self\",c,e.CBCM,t,a]}]},{cN:\"class\",bK:\"class interface\",e:\"{\",eE:!0,i:/[:\\(\\$\"]/,c:[{bK:\"extends implements\"},e.UTM]},{bK:\"namespace\",e:\";\",i:/[\\.']/,c:[e.UTM]},{bK:\"use\",e:\";\",c:[e.UTM]},{b:\"=>\"},t,a]}});hljs.registerLanguage(\"makefile\",function(e){var i={cN:\"variable\",v:[{b:\"\\\\$\\\\(\"+e.UIR+\"\\\\)\",c:[e.BE]},{b:/\\$[@%<?\\^\\+\\*]/}]},r={cN:\"string\",b:/\"/,e:/\"/,c:[e.BE,i]},a={cN:\"variable\",b:/\\$\\([\\w-]+\\s/,e:/\\)/,k:{built_in:\"subst patsubst strip findstring filter filter-out sort word wordlist firstword lastword dir notdir suffix basename addsuffix addprefix join wildcard realpath abspath error warning shell origin flavor foreach if or and call eval file value\"},c:[i]},n={b:\"^\"+e.UIR+\"\\\\s*(?=[:+?]?=)\"},t={cN:\"section\",b:/^[^\\s]+:/,e:/$/,c:[i]};return{aliases:[\"mk\",\"mak\"],k:\"define endef undefine ifdef ifndef ifeq ifneq else endif include -include sinclude override export unexport private vpath\",l:/[\\w-]+/,c:[e.HCM,i,r,a,n,{cN:\"meta\",b:/^\\.PHONY:/,e:/$/,k:{\"meta-keyword\":\".PHONY\"},l:/[\\.\\w]+/},t]}});hljs.registerLanguage(\"cs\",function(e){var a={keyword:\"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let nameof on orderby partial remove select set value var when where yield\",literal:\"null false true\"},i={cN:\"number\",v:[{b:\"\\\\b(0b[01']+)\"},{b:\"(-?)\\\\b([\\\\d']+(\\\\.[\\\\d']*)?|\\\\.[\\\\d']+)(u|U|l|L|ul|UL|f|F|b|B)\"},{b:\"(-?)(\\\\b0[xX][a-fA-F0-9']+|(\\\\b[\\\\d']+(\\\\.[\\\\d']*)?|\\\\.[\\\\d']+)([eE][-+]?[\\\\d']+)?)\"}],relevance:0},c={cN:\"string\",b:'@\"',e:'\"',c:[{b:'\"\"'}]},r=e.inherit(c,{i:/\\n/}),n={cN:\"subst\",b:\"{\",e:\"}\",k:a},t=e.inherit(n,{i:/\\n/}),s={cN:\"string\",b:/\\$\"/,e:'\"',i:/\\n/,c:[{b:\"{{\"},{b:\"}}\"},e.BE,t]},l={cN:\"string\",b:/\\$@\"/,e:'\"',c:[{b:\"{{\"},{b:\"}}\"},{b:'\"\"'},n]},b=e.inherit(l,{i:/\\n/,c:[{b:\"{{\"},{b:\"}}\"},{b:'\"\"'},t]});n.c=[l,s,c,e.ASM,e.QSM,i,e.CBCM],t.c=[b,s,r,e.ASM,e.QSM,i,e.inherit(e.CBCM,{i:/\\n/})];var o={v:[l,s,c,e.ASM,e.QSM]},d=e.IR+\"(<\"+e.IR+\"(\\\\s*,\\\\s*\"+e.IR+\")*>)?(\\\\[\\\\])?\";return{aliases:[\"csharp\",\"c#\"],k:a,i:/::/,c:[e.C(\"///\",\"$\",{rB:!0,c:[{cN:\"doctag\",v:[{b:\"///\",relevance:0},{b:\"\\x3c!--|--\\x3e\"},{b:\"</?\",e:\">\"}]}]}),e.CLCM,e.CBCM,{cN:\"meta\",b:\"#\",e:\"$\",k:{\"meta-keyword\":\"if else elif endif define undef warning error line region endregion pragma checksum\"}},o,i,{bK:\"class interface\",e:/[{;=]/,i:/[^\\s:,]/,c:[e.TM,e.CLCM,e.CBCM]},{bK:\"namespace\",e:/[{;=]/,i:/[^\\s:]/,c:[e.inherit(e.TM,{b:\"[a-zA-Z](\\\\.?\\\\w)*\"}),e.CLCM,e.CBCM]},{cN:\"meta\",b:\"^\\\\s*\\\\[\",eB:!0,e:\"\\\\]\",eE:!0,c:[{cN:\"meta-string\",b:/\"/,e:/\"/}]},{bK:\"new return throw await else\",relevance:0},{cN:\"function\",b:\"(\"+d+\"\\\\s+)+\"+e.IR+\"\\\\s*\\\\(\",rB:!0,e:/\\s*[{;=]/,eE:!0,k:a,c:[{b:e.IR+\"\\\\s*\\\\(\",rB:!0,c:[e.TM],relevance:0},{cN:\"params\",b:/\\(/,e:/\\)/,eB:!0,eE:!0,k:a,relevance:0,c:[o,i,e.CBCM]},e.CLCM,e.CBCM]}]}});hljs.registerLanguage(\"kotlin\",function(e){var t={keyword:\"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual trait volatile transient native default\",built_in:\"Byte Short Char Int Long Boolean Float Double Void Unit Nothing\",literal:\"true false null\"},a={cN:\"symbol\",b:e.UIR+\"@\"},n={cN:\"subst\",b:\"\\\\${\",e:\"}\",c:[e.CNM]},c={cN:\"variable\",b:\"\\\\$\"+e.UIR},r={cN:\"string\",v:[{b:'\"\"\"',e:'\"\"\"(?=[^\"])',c:[c,n]},{b:\"'\",e:\"'\",i:/\\n/,c:[e.BE]},{b:'\"',e:'\"',i:/\\n/,c:[e.BE,c,n]}]};n.c.push(r);var i={cN:\"meta\",b:\"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\\\s*:(?:\\\\s*\"+e.UIR+\")?\"},l={cN:\"meta\",b:\"@\"+e.UIR,c:[{b:/\\(/,e:/\\)/,c:[e.inherit(r,{cN:\"meta-string\"})]}]},s={cN:\"number\",b:\"\\\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\\\d]+[\\\\d_]+[\\\\d]+|[\\\\d]+)(\\\\.([\\\\d]+[\\\\d_]+[\\\\d]+|[\\\\d]+))?|\\\\.([\\\\d]+[\\\\d_]+[\\\\d]+|[\\\\d]+))([eE][-+]?\\\\d+)?)[lLfF]?\",relevance:0},b=e.C(\"/\\\\*\",\"\\\\*/\",{c:[e.CBCM]}),o={v:[{cN:\"type\",b:e.UIR},{b:/\\(/,e:/\\)/,c:[]}]},d=o;return d.v[1].c=[o],o.v[1].c=[d],{aliases:[\"kt\"],k:t,c:[e.C(\"/\\\\*\\\\*\",\"\\\\*/\",{relevance:0,c:[{cN:\"doctag\",b:\"@[A-Za-z]+\"}]}),e.CLCM,b,{cN:\"keyword\",b:/\\b(break|continue|return|this)\\b/,starts:{c:[{cN:\"symbol\",b:/@\\w+/}]}},a,i,l,{cN:\"function\",bK:\"fun\",e:\"[(]|$\",rB:!0,eE:!0,k:t,i:/fun\\s+(<.*>)?[^\\s\\(]+(\\s+[^\\s\\(]+)\\s*=/,relevance:5,c:[{b:e.UIR+\"\\\\s*\\\\(\",rB:!0,relevance:0,c:[e.UTM]},{cN:\"type\",b:/</,e:/>/,k:\"reified\",relevance:0},{cN:\"params\",b:/\\(/,e:/\\)/,endsParent:!0,k:t,relevance:0,c:[{b:/:/,e:/[=,\\/]/,eW:!0,c:[o,e.CLCM,b],relevance:0},e.CLCM,b,i,l,r,e.CNM]},b]},{cN:\"class\",bK:\"class interface trait\",e:/[:\\{(]|$/,eE:!0,i:\"extends implements\",c:[{bK:\"public protected internal private constructor\"},e.UTM,{cN:\"type\",b:/</,e:/>/,eB:!0,eE:!0,relevance:0},{cN:\"type\",b:/[,:]\\s*/,e:/[<\\(,]|$/,eB:!0,rE:!0},i,l]},r,{cN:\"meta\",b:\"^#!/usr/bin/env\",e:\"$\",i:\"\\n\"},s]}});hljs.registerLanguage(\"plaintext\",function(e){return{disableAutodetect:!0}});hljs.registerLanguage(\"javascript\",function(e){var r=\"<>\",a=\"</>\",t={b:/<[A-Za-z0-9\\\\._:-]+/,e:/\\/[A-Za-z0-9\\\\._:-]+>|\\/>/},c=\"[A-Za-z$_][0-9A-Za-z$_]*\",n={keyword:\"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as\",literal:\"true false null undefined NaN Infinity\",built_in:\"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise\"},s={cN:\"number\",v:[{b:\"\\\\b(0[bB][01]+)n?\"},{b:\"\\\\b(0[oO][0-7]+)n?\"},{b:e.CNR+\"n?\"}],relevance:0},o={cN:\"subst\",b:\"\\\\$\\\\{\",e:\"\\\\}\",k:n,c:[]},i={b:\"html`\",e:\"\",starts:{e:\"`\",rE:!1,c:[e.BE,o],sL:\"xml\"}},b={b:\"css`\",e:\"\",starts:{e:\"`\",rE:!1,c:[e.BE,o],sL:\"css\"}},l={cN:\"string\",b:\"`\",e:\"`\",c:[e.BE,o]};o.c=[e.ASM,e.QSM,i,b,l,s,e.RM];var u=o.c.concat([e.CBCM,e.CLCM]);return{aliases:[\"js\",\"jsx\",\"mjs\",\"cjs\"],k:n,c:[{cN:\"meta\",relevance:10,b:/^\\s*['\"]use (strict|asm)['\"]/},{cN:\"meta\",b:/^#!/,e:/$/},e.ASM,e.QSM,i,b,l,e.CLCM,e.C(\"/\\\\*\\\\*\",\"\\\\*/\",{relevance:0,c:[{cN:\"doctag\",b:\"@[A-Za-z]+\",c:[{cN:\"type\",b:\"\\\\{\",e:\"\\\\}\",relevance:0},{cN:\"variable\",b:c+\"(?=\\\\s*(-)|$)\",endsParent:!0,relevance:0},{b:/(?=[^\\n])\\s/,relevance:0}]}]}),e.CBCM,s,{b:/[{,\\n]\\s*/,relevance:0,c:[{b:c+\"\\\\s*:\",rB:!0,relevance:0,c:[{cN:\"attr\",b:c,relevance:0}]}]},{b:\"(\"+e.RSR+\"|\\\\b(case|return|throw)\\\\b)\\\\s*\",k:\"return throw case\",c:[e.CLCM,e.CBCM,e.RM,{cN:\"function\",b:\"(\\\\(.*?\\\\)|\"+c+\")\\\\s*=>\",rB:!0,e:\"\\\\s*=>\",c:[{cN:\"params\",v:[{b:c},{b:/\\(\\s*\\)/},{b:/\\(/,e:/\\)/,eB:!0,eE:!0,k:n,c:u}]}]},{cN:\"\",b:/\\s/,e:/\\s*/,skip:!0},{v:[{b:r,e:a},{b:t.b,e:t.e}],sL:\"xml\",c:[{b:t.b,e:t.e,skip:!0,c:[\"self\"]}]}],relevance:0},{cN:\"function\",bK:\"function\",e:/\\{/,eE:!0,c:[e.inherit(e.TM,{b:c}),{cN:\"params\",b:/\\(/,e:/\\)/,eB:!0,eE:!0,c:u}],i:/\\[|%/},{b:/\\$[(.]/},e.METHOD_GUARD,{cN:\"class\",bK:\"class\",e:/[{;=]/,eE:!0,i:/[:\"\\[\\]]/,c:[{bK:\"extends\"},e.UTM]},{bK:\"constructor get set\",e:/\\{/,eE:!0}],i:/#(?!!)/}});hljs.registerLanguage(\"json\",function(e){var i={literal:\"true false null\"},n=[e.CLCM,e.CBCM],c=[e.QSM,e.CNM],r={e:\",\",eW:!0,eE:!0,c:c,k:i},t={b:\"{\",e:\"}\",c:[{cN:\"attr\",b:/\"/,e:/\"/,c:[e.BE],i:\"\\\\n\"},e.inherit(r,{b:/:/})].concat(n),i:\"\\\\S\"},a={b:\"\\\\[\",e:\"\\\\]\",c:[e.inherit(r)],i:\"\\\\S\"};return c.push(t,a),n.forEach(function(e){c.push(e)}),{c:c,k:i,i:\"\\\\S\"}});hljs.registerLanguage(\"css\",function(e){var c={b:/(?:[A-Z\\_\\.\\-]+|--[a-zA-Z0-9_-]+)\\s*:/,rB:!0,e:\";\",eW:!0,c:[{cN:\"attribute\",b:/\\S/,e:\":\",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\\w-]+\\(/,rB:!0,c:[{cN:\"built_in\",b:/[\\w-]+/},{b:/\\(/,e:/\\)/,c:[e.ASM,e.QSM,e.CSSNM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:\"number\",b:\"#[0-9A-Fa-f]+\"},{cN:\"meta\",b:\"!important\"}]}}]};return{cI:!0,i:/[=\\/|'\\$]/,c:[e.CBCM,{cN:\"selector-id\",b:/#[A-Za-z0-9_-]+/},{cN:\"selector-class\",b:/\\.[A-Za-z0-9_-]+/},{cN:\"selector-attr\",b:/\\[/,e:/\\]/,i:\"$\",c:[e.ASM,e.QSM]},{cN:\"selector-pseudo\",b:/:(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\"'.]+/},{b:\"@(page|font-face)\",l:\"@[a-z-]+\",k:\"@page @font-face\"},{b:\"@\",e:\"[{;]\",i:/:/,rB:!0,c:[{cN:\"keyword\",b:/@\\-?\\w[\\w]*(\\-\\w+)*/},{b:/\\s/,eW:!0,eE:!0,relevance:0,k:\"and or not only\",c:[{b:/[a-z-]+:/,cN:\"attribute\"},e.ASM,e.QSM,e.CSSNM]}]},{cN:\"selector-tag\",b:\"[a-zA-Z-][a-zA-Z0-9_-]*\",relevance:0},{b:\"{\",e:\"}\",i:/\\S/,c:[e.CBCM,c]}]}});hljs.registerLanguage(\"objectivec\",function(e){var t=/[a-zA-Z@][a-zA-Z0-9_]*/,i=\"@interface @class @protocol @implementation\";return{aliases:[\"mm\",\"objc\",\"obj-c\"],k:{keyword:\"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN\",literal:\"false true FALSE TRUE nil YES NO NULL\",built_in:\"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once\"},l:t,i:\"</\",c:[{cN:\"built_in\",b:\"\\\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\\\w+\"},e.CLCM,e.CBCM,e.CNM,e.QSM,e.ASM,{cN:\"string\",v:[{b:'@\"',e:'\"',i:\"\\\\n\",c:[e.BE]}]},{cN:\"meta\",b:/#\\s*[a-z]+\\b/,e:/$/,k:{\"meta-keyword\":\"if else elif endif define undef warning error line pragma ifdef ifndef include\"},c:[{b:/\\\\\\n/,relevance:0},e.inherit(e.QSM,{cN:\"meta-string\"}),{cN:\"meta-string\",b:/<.*?>/,e:/$/,i:\"\\\\n\"},e.CLCM,e.CBCM]},{cN:\"class\",b:\"(\"+i.split(\" \").join(\"|\")+\")\\\\b\",e:\"({|$)\",eE:!0,k:i,l:t,c:[e.UTM]},{b:\"\\\\.\"+e.UIR,relevance:0}]}});hljs.registerLanguage(\"sql\",function(e){var t=e.C(\"--\",\"$\");return{cI:!0,i:/[<>{}*]/,c:[{bK:\"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment values with\",e:/;/,eW:!0,l:/[\\w\\.]+/,k:{keyword:\"as abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias all allocate allow alter always analyze ancillary and anti any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound bucket buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain explode export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force foreign form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour hours http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lateral lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minutes minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notnull notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second seconds section securefile security seed segment select self semi sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tablesample tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unnest unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace window with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek\",literal:\"true false null unknown\",built_in:\"array bigint binary bit blob bool boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text time timestamp tinyint varchar varchar2 varying void\"},c:[{cN:\"string\",b:\"'\",e:\"'\",c:[{b:\"''\"}]},{cN:\"string\",b:'\"',e:'\"',c:[{b:'\"\"'}]},{cN:\"string\",b:\"`\",e:\"`\"},e.CNM,e.CBCM,t,e.HCM]},e.CBCM,t,e.HCM]}});hljs.registerLanguage(\"http\",function(e){var t=\"HTTP/[0-9\\\\.]+\";return{aliases:[\"https\"],i:\"\\\\S\",c:[{b:\"^\"+t,e:\"$\",c:[{cN:\"number\",b:\"\\\\b\\\\d{3}\\\\b\"}]},{b:\"^[A-Z]+ (.*?) \"+t+\"$\",rB:!0,e:\"$\",c:[{cN:\"string\",b:\" \",e:\" \",eB:!0,eE:!0},{b:t},{cN:\"keyword\",b:\"[A-Z]+\"}]},{cN:\"attribute\",b:\"^\\\\w\",e:\": \",eE:!0,i:\"\\\\n|\\\\s|=\",starts:{e:\"$\",relevance:0}},{b:\"\\\\n\\\\n\",starts:{sL:[],eW:!0}}]}});hljs.registerLanguage(\"diff\",function(e){return{aliases:[\"patch\"],c:[{cN:\"meta\",relevance:10,v:[{b:/^@@ +\\-\\d+,\\d+ +\\+\\d+,\\d+ +@@$/},{b:/^\\*\\*\\* +\\d+,\\d+ +\\*\\*\\*\\*$/},{b:/^\\-\\-\\- +\\d+,\\d+ +\\-\\-\\-\\-$/}]},{cN:\"comment\",v:[{b:/Index: /,e:/$/},{b:/={3,}/,e:/$/},{b:/^\\-{3}/,e:/$/},{b:/^\\*{3} /,e:/$/},{b:/^\\+{3}/,e:/$/},{b:/^\\*{15}$/}]},{cN:\"addition\",b:\"^\\\\+\",e:\"$\"},{cN:\"deletion\",b:\"^\\\\-\",e:\"$\"},{cN:\"addition\",b:\"^\\\\!\",e:\"$\"}]}});"
  },
  {
    "path": "app/src/main/assets/js/lazyload.js",
    "content": "/*\n * 图片懒加载, 用于 image.js;\n * 对 Unveil 进行了一定的修改\n *\n * jQuery Unveil\n * A very lightweight jQuery plugin to lazy load images\n * http://luis-almeida.github.com/unveil\n *\n * Licensed under the MIT license.\n * Copyright 2013 Luís Almeida\n * https://github.com/luis-almeida\n */\n;(function($) {\n\t$.fn.unveil = function(threshold, callback) {\n\t\tvar $w = $(window),\n\t\t\tth = threshold || 0,\n\t\t\t//保存当前所有未显示的图片列表；后面会更新\n\t\t\timages = this,\n\t\t\tloaded;\n\n\t\t//对所有的图片绑定unveil事件,当图片触发unveil事件的时候，读取图片的original-src属性进行src赋值显示\n\t\tthis.one('unveil', function() {\n\t\t\tvar source = this.getAttribute('original-src');\n\t\t\tif (source) {\n\t\t\t\tif (typeof callback === 'function') {\n\t\t\t\t\tcallback.call(this, source);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\t// 此函数用来读取当前屏幕内的所有图片,并且触发元素的unveil事件,之后将这些图片从全局中去除掉\n\t\tfunction unveil() {\n\t\t\tvar inview = images.filter(function() {\n\t\t\t\tvar $e = $(this);\n\t\t\t\tif ($e.css('display') == 'hidden') {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tvar wt = $w.scrollTop(),\n\t\t\t\t\twb = wt + $w.height(),\n\t\t\t\t\tet = $e.offset().top,\n\t\t\t\t\teb = et + $e.height();\n\n\t\t\t\treturn eb >= wt - th && et <= wb + th;\n\t\t\t});\n\n\t\t\tloaded = inview.trigger('unveil');\n\t\t\timages = images.not(loaded);\n\t\t}\n\n\t\t//针对.unveil元素的绑定scroll/resize/lookup事件，一旦触发此事件则执行unveil函数\n\t\t$w.on('scroll.unveil resize.unveil lookup.unveil', unveil);\n\t\t//页面初始化的时候执行一次unveil\n\t\tunveil();\n\t\treturn this;\n\t};\n})(window.Zepto);"
  },
  {
    "path": "app/src/main/assets/js/media.js",
    "content": "/*\n * 设置图片的默认加载行为\n */\n\n// ArticleBridge.log(\"触发脚本\" );\n// 在这里调用是因为在第一次打开ArticleActivity时，渲染WebView的内容比较慢，此时在ArticleActivity中调用setupImage不会执行。\n// 不直接执行setupImage是因为在viewpager中预加载而生成webview的时候，这里的懒加载就被触发了，3个webview首屏的图片就都被触发下载了\nsetTimeout( optimize(),10 );\n\nfunction optimize() {\n\thandleImage();\n\thandleQQVideoUrl();\n\thandleVideo();\n\thandleIFrame();\n\thandleEmbed();\n\thandleAudio();\n\thandleTable();\n}\n\n\nfunction handleImage() {\n\tvar articleId = $('article').attr('id');\n\n\t$('img').each(function() {\n\t\tvar image = $(this);\n\t\tvar originalUrl = image.attr('original-src');\n\t\tif( originalUrl == null || originalUrl == \"\" || originalUrl == undefined ){\n\t\t\treturn true;\n\t\t}\n\t\tvar url = image.attr('src');\n\t\t// 为什么用 hashCode 作为图片的 id 来传递，而不是 src, window.btoa(url)？\n\t\t// 1、这里获得的src是经过转义的，而传递到java层再传回来的src是未经过转义的（特别是中文）。\n\t\t// 2、window.btoa(url) 中 url 的字符不能超出 0x00~0xFF 范围（不能有中文或特殊字符），否则会有无效字符异常。\n\t\timage.attr('id', hashCode(originalUrl) );\n\n\t\timage.unveil(200, function() {\n\t\t    var image = $(this);\n\t\t    if(!image.hasClass(\"image-holder\")){\n\t\t\t\timage.addClass(\"image-holder\");\n\t\t\t\t//ArticleBridge.log(\"触发脚本 + 加载\" + articleId + \"  , \" + image.attr('referrerpolicy') );\n\t\t\t\tArticleBridge.readImage(articleId, image.attr('id'), originalUrl);\n\t\t    }\n\t\t});\n\t});\n\n\t$('img').click(function(event) {\n\t\tvar image = $(this);\n\t\tvar displayUrl = image.attr('src');\n\t\tvar originalUrl = image.attr('original-src');\n\t\t// 此时去下载图片\n\t\tif (displayUrl == IMAGE_HOLDER_CLICK_TO_LOAD_URL) {\n\t\t\timage.attr('src', IMAGE_HOLDER_LOADING_URL);\n\t\t\tArticleBridge.downImage(articleId, image.attr('id'), originalUrl, false);\n\t\t}else if (displayUrl == IMAGE_HOLDER_LOAD_FAILED_URL){\n\t\t\timage.attr('src', IMAGE_HOLDER_LOADING_URL);\n\t\t\tArticleBridge.downImage(articleId, image.attr('id'), originalUrl, false);\n\t\t}else if (displayUrl == IMAGE_HOLDER_IMAGE_ERROR_URL){\n\t\t\timage.attr('src', IMAGE_HOLDER_LOADING_URL);\n\t\t\tArticleBridge.downImage(articleId, image.attr('id'), originalUrl, true);\n\t\t}else if (displayUrl != IMAGE_HOLDER_LOADING_URL){ // 由于此时正在加载中所以不处理\n\t\t\tArticleBridge.openImage(articleId, displayUrl);\n\t\t}\n\t\t// 阻止元素发生默认的行为（例如点击提交按钮时阻止对表单的提交）\n\t\tevent.preventDefault();\n\t\t// 停止事件传播，阻止它被分派到其他 Document 节点。在事件传播的任何阶段都可以调用它。\n\t\t// 注意，虽然该方法不能阻止同一个 Document 节点上的其他事件句柄被调用，但是它可以阻止把事件分派到其他节点。\n\t\tevent.stopPropagation();\n\t});\n}\n\n// 将老的QQ视频链接换成新的\nfunction handleQQVideoUrl() {\n\tvar list = document.querySelectorAll('iframe[src^=\"http://v.qq.com/iframe/player.html\"],iframe[src^=\"https://v.qq.com/iframe/player.html\"]');\n\tfor (var i = 0,len = list.length; i < len; i++) {\n\t\tlist[i].src = list[i].src.replace('v.qq.com/iframe/player.html', 'v.qq.com/txp/iframe/player.html');\n\t}\n}\n\n// 针对 iframe 标签做处理\nfunction handleIFrame(){\n\t$('iframe').each(function() {\n\t\tvar frame = $(this);\n\t\tframe.removeAttr(\"sandbox\");// sandbox 会限制 iframe 的各种能力\n\t\tframe.attr(\"frameborder\", \"0\");\n\t\tframe.attr(\"allowfullscreen\", \"\");\n\t\tframe.attr(\"scrolling\", \"no\");\n\t\tframe.attr(\"src\", frame.attr(\"src\").replace(/(width|height)=\\d+/ig, \"\").replace(/(&(amp;)*){2,}/ig, \"&\"));\n\t\t// 让iframe默认为点击新窗口打开\n\t\tframe.attr(\"style\", \"pointer-events:none;\");\n\t\tframe.wrap('<figure class=\"iframe_wrap\"></figure>');\n\t\tframe.parent().click(function(event) {\n\t\t\tArticleBridge.openLink(frame.attr(\"src\"));\n\t\t\tevent.preventDefault();\n\t\t});\n\t\t// 当iframe加载完毕后，根据src来判断是否需要关闭新窗口打开\n\t\tframe.on('load', function() {\n\t\t\tif( loadOnInner(frame.attr('src')) ){\n\t\t\t\t$(this).attr(\"style\", \"pointer-events:auto;\");\n\t\t\t}\n\t\t});\n\t});\n}\nfunction handleEmbed(){\n\t$('embed').each(function() {\n\t\tvar frame = $(this);\n\t\tframe.attr(\"autostart\",\"1\");\n\t\tframe.attr(\"src\", frame.attr(\"src\").replace(/(width|height)=\\d+/ig, \"\").replace(/(&(amp;)*){2,}/ig, \"&\"));\n\t\tframe.attr(\"style\", \"pointer-events:none;\");\n\t\tframe.wrap('<figure class=\"embed_wrap\"></figure>');\n\t\tframe.parent().click(function(event) {\n\t\t\tArticleBridge.openLink(frame.attr(\"src\"));\n\t\t\tevent.preventDefault();\n\t\t});\n\t\t// 当iframe加载完毕后，根据src来判断是否需要关闭新窗口打开\n\t\tframe.on('load', function() {\n\t\t\tif( loadOnInner(frame.attr('src')) ){\n\t\t\t\t$(this).attr(\"style\", \"pointer-events:auto;\");\n\t\t\t}\n\t\t});\n\t});\n}\nfunction handleAudio(){\n\t$('audio').each(function() {\n\t\tvar audio = $(this);\n\t\taudio.attr(\"controls\", \"true\");\n        audio.attr(\"width\", \"100%\")\n\t\taudio.attr(\"style\", \"pointer-events:none;\");\n\t\taudio.wrap('<div class=\"audio_wrap\"></div>');\n\t\taudio.parent().click(function(event) {\n\t\t\tArticleBridge.openAudio( audio.attr(\"src\") );\n\t\t\tevent.preventDefault();\n\t\t});\n\t});\n}\nfunction handleVideo(){\n\t$('video').each(function() {\n\t\tvar video = $(this);\n\t\tvideo.attr(\"controls\", \"true\");\n\t\tvideo.attr(\"width\", \"100%\");\n        video.attr(\"height\", \"auto\");\n        video.attr(\"preload\", \"metadata\");\n\t\tvideo.wrap('<div class=\"video_wrap\"></div>');\n\t});\n}\nfunction handleTable(){\n\t$('table').each(function() {\n\t\t$(this).wrap('<div class=\"table_wrap\"></div>');\n\t});\n}\n\nfunction loadOnInner(url){\n\tvar flags = [\"music.163.com/outchain/player\",\"player.bilibili.com/player.html\",\"bilibili.com/blackboard/html5mobileplayer.html\",\"player.youku.com\",\"youtube.com/embed\",\"open.iqiyi.com\",\"v.qq.com\",\"letv.com\",\"sohu.com\",\"fpie1.com/#/video\",\"fpie2.com/#/video\",\"www.google.com/maps/embed\"];\n\tfor (var i = 0; i < flags.length; i++) {\n\t\tif (url.indexOf(flags[i]) != -1 ){\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\nfunction findImageById(imgId) {\n\treturn $('img[id=\"' + imgId + '\"]');\n}\n\nfunction onImageLoadNeedClick(imgId) {\n\tvar image = findImageById(imgId);\n\tif (image) {\n\t\timage.attr('src', IMAGE_HOLDER_CLICK_TO_LOAD_URL);\n\t}\n\n}\nfunction onImageLoading(imgId) {\n\tvar image = findImageById(imgId);\n\tif (image) {\n\t\timage.attr('src', IMAGE_HOLDER_LOADING_URL);\n\t}\n}\nfunction onImageLoadFailed(imgId) {\n\tvar image = findImageById(imgId);\n\tif (image) {\n\t\timage.attr('src', IMAGE_HOLDER_LOAD_FAILED_URL);\n\t}\n}\nfunction onImageError(imgId) {\n\tvar image = findImageById(imgId);\n\tif (image) {\n\t\timage.attr('src', IMAGE_HOLDER_IMAGE_ERROR_URL);\n\t}\n}\nfunction onImageLoadSuccess(imgId, displayUrl) {\n\tvar image = findImageById(imgId);\n\timage.attr('src', displayUrl);\n}\n\n//产生一个hash值，只有数字，规则和java的hashcode规则相同\nfunction hashCode(str){\n\tvar h = 0;\n\tvar len = str.length;\n\tfor(var i = 0; i < len; i++){\n\t\tvar tmp=str.charCodeAt(i);\n\t\th = 31 * h  + tmp;\n\t\tif(h>0x7fffffff || h<0x80000000){\n\t\t\th=h & 0xffffffff;\n\t\t}\n\t}\n\t// 之所以用字符串格式，是因为通过$(this).attr('id')获取到的是字符串格式。\n\treturn (h).toString();\n};\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/App.java",
    "content": "package me.wizos.loread;\n\nimport android.app.Application;\nimport android.content.Intent;\nimport android.os.AsyncTask;\nimport android.text.TextUtils;\nimport android.util.DisplayMetrics;\nimport android.webkit.WebView;\n\nimport androidx.work.WorkManager;\n\nimport com.bumptech.glide.Glide;\nimport com.carlt.networklibs.NetType;\nimport com.carlt.networklibs.NetworkManager;\nimport com.carlt.networklibs.annotation.NetWork;\nimport com.carlt.networklibs.utils.Constants;\nimport com.hjq.toast.ToastUtils;\nimport com.hjq.toast.style.ToastAliPayStyle;\nimport com.lzy.okgo.OkGo;\nimport com.orhanobut.logger.AndroidLogAdapter;\nimport com.orhanobut.logger.Logger;\nimport com.socks.library.KLog;\nimport com.tencent.bugly.crashreport.CrashReport;\nimport com.tencent.stat.MtaSDkException;\nimport com.tencent.stat.StatConfig;\nimport com.tencent.stat.StatCrashReporter;\nimport com.tencent.stat.StatService;\nimport com.tencent.stat.common.StatConstants;\nimport com.yhao.floatwindow.view.FloatWindow;\n\nimport java.io.File;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\nimport me.wizos.loread.activity.SplashActivity;\nimport me.wizos.loread.adapter.ArticlePagedListAdapter;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.CorePref;\nimport me.wizos.loread.db.User;\nimport me.wizos.loread.network.api.AuthApi;\nimport me.wizos.loread.network.api.BaseApi;\nimport me.wizos.loread.network.api.FeedlyApi;\nimport me.wizos.loread.network.api.InoReaderApi;\nimport me.wizos.loread.network.api.LoreadApi;\nimport me.wizos.loread.network.api.OAuthApi;\nimport me.wizos.loread.network.api.TinyRSSApi;\nimport me.wizos.loread.utils.FileUtil;\nimport me.wizos.loread.utils.NetworkUtil;\nimport me.wizos.loread.utils.ScriptUtil;\nimport me.wizos.loread.utils.Tool;\nimport me.wizos.loread.view.WebViewS;\n\nimport static me.wizos.loread.utils.NetworkUtil.NETWORK_MOBILE;\nimport static me.wizos.loread.utils.NetworkUtil.NETWORK_NONE;\nimport static me.wizos.loread.utils.NetworkUtil.NETWORK_WIFI;\n\n/**\n * 在Android中，可以通过继承Application类来实现应用程序级的全局变量，这种全局变量方法相对静态类更有保障，直到应用的所有Activity全部被destory掉之后才会被释放掉。\n * <p> App 类是全局的单例\n * Created by Wizos on 2015/12/24.\n */\npublic class App extends Application implements Thread.UncaughtExceptionHandler {\n    private static String TAG = \"App\";\n    private static App instance;\n    public static final String CATEGORY_ALL = \"/category/global.all\";\n    public static final String CATEGORY_UNCATEGORIZED = \"/category/global.uncategorized\";\n    public static final String CATEGORY_TAG = \"/category/global.tag\";\n    public static final String CATEGORY_SEARCH = \"/category/global.search\";\n\n    public static final String CATEGORY_STARED = \"/tag/global.saved\";\n    public static final String CATEGORY_MUST = \"/category/global.must\";\n\n    public static final String Referer = \"Referer\";\n\n    public static final String DISPLAY_RSS = \"rss\";\n    public static final String DISPLAY_READABILITY = \"readability\";\n    public static final String DISPLAY_LINK = \"webpage\";\n    public static final int OPEN_MODE_RSS = 0;\n    public static final int OPEN_MODE_LINK = 1;\n    public static final int OPEN_MODE_READABILITY = 2;\n\n    public static final int STATUS_NOT_FILED = 0;\n    public static final int STATUS_TO_BE_FILED = 1;\n    public static final int STATUS_IS_FILED = 2;\n\n    public static final String NOT_FILED = \"cache\";\n    public static final String TO_BE_FILED = \"box\";\n    public static final String IS_FILED = \"store\";\n\n    public static final int ActivityResult_LoginPageToProvider = 1;\n    public static final int ActivityResult_ArtToMain = 2;\n    public static final int ActivityResult_SearchLocalArtsToMain = 3;\n\n    public static final int TYPE_GROUP = 0;\n    public static final int TYPE_FEED = 1;\n\n    // 状态为所有\n    public static final int STATUS_ALL = 0;\n    // 0 未读\n    public static final int STATUS_UNREAD = 1;\n    // 1 已读\n    public static final int STATUS_READED = 2;\n    // 00 强制未读\n    public static final int STATUS_UNREADING = 3;\n    // 为什么这里不用1和0？因为在用streamStatus时，会综合readStatus和starStatus，如果有重复的就会取不到\n    // 类似 StreamIds 将 feed,category,tag 综合考虑，StreamStatus是将 readStatus,starStatus 综合在一起考虑\n    public static final int STATUS_STARED = 4;\n    public static final int STATUS_UNSTAR = 5;\n\n    public static final int MSG_DOUBLE_TAP = -1;\n\n    public int screenWidth;\n    public int screenHeight;\n\n    public final static int THEME_DAY = 0;\n    public final static int THEME_NIGHT = 1;\n\n    public ArticlePagedListAdapter articlesAdapter;\n\n    // public boolean isSyncing = false;\n    public static String webViewBaseUrl;\n\n    public LinkedHashMap<String, Integer> articleProgress = new LinkedHashMap<String, Integer>() {\n        @Override\n        protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {\n            return size() > 16;\n        }\n    };\n\n\n    public static App i() {\n        if (instance == null) { // 双重锁定，只有在 withDB 还没被初始化的时候才会进入到下一行，然后加上同步锁\n            synchronized (App.class) { // 同步锁，避免多线程时可能 new 出两个实例的情况\n                if (instance == null) {\n                    instance = new App();\n                }\n            }\n        }\n        return instance;\n    }\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        instance = this;\n        initVar();\n\n        ToastUtils.init(this, new ToastAliPayStyle(this));\n\n        CoreDB.init(instance);\n\n        // 【提前初始化 WebView 内核】由于其内部会调用 Looper ，不能放在子线程中\n        // 链接：https://www.jianshu.com/p/fc7909e24178\n        // 经过测试，采用Application要比采用Activity的context要少用20~30M左右的内存。但采用Application会影响在 webview 中打开对话框。\n        WebViewS articleWebView = new WebViewS(this);\n        articleWebView.destroy();\n\n        if (BuildConfig.DEBUG) {\n            WebView.setWebContentsDebuggingEnabled(true);\n        }\n\n        Logger.addLogAdapter(new AndroidLogAdapter());\n\n        initCrashReport();\n\n        AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {\n            @Override\n            public void run() {\n                FloatWindow.init(instance);\n                // 初始化网络框架\n                OkGo.getInstance().init(instance);\n                // 监听子线程的报错\n                Thread.setDefaultUncaughtExceptionHandler(instance);\n                // 初始化统计&监控服务\n                KLog.init(BuildConfig.DEBUG);\n                // initLeakCanary();\n\n                ScriptUtil.init();\n\n                // 初始化网络状态\n                NetworkUtil.getNetWorkState();\n\n                // 尽可能早的进行这一步操作, 建议在 Application 中完成初始化操作\n                NetworkManager.getInstance().init(instance);\n                //注册\n                NetworkManager.getInstance().registerObserver(instance);\n\n                // 保险套\n                // CondomProcess.installExceptDefaultProcess(instance);\n            }\n        });\n    }\n\n    /**\n     * Application结束的时候会调用,由系统决定调用的时机\n     */\n    @Override\n    public void onTerminate() {\n        super.onTerminate();\n        NetworkManager.getInstance().unRegisterObserver(this);\n    }\n\n\n    //所有网络变化都会被调用，可以通过 NetType 来判断当前网络具体状态\n    @NetWork(netType = NetType.AUTO)\n    public void network(NetType netType) {\n        switch (netType) {\n            case WIFI:\n                KLog.e(Constants.LOG_TAG, \"wifi\");\n                NetworkUtil.setTheNetwork(NETWORK_WIFI);\n                break;\n            case CMNET:\n            case CMWAP:\n                KLog.e(Constants.LOG_TAG, \"4G\");\n                NetworkUtil.setTheNetwork(NETWORK_MOBILE);\n                break;\n            case AUTO:\n                KLog.e(Constants.LOG_TAG, \"自动\");\n                break;\n            case NONE:\n                KLog.e(Constants.LOG_TAG, \"无网络\");\n                NetworkUtil.setTheNetwork(NETWORK_NONE);\n                break;\n            default:\n                break;\n        }\n    }\n\n    @Override\n    public void uncaughtException(Thread thread, Throwable ex) {\n        KLog.e(\"线程意外报错\");\n        ex.printStackTrace();\n    }\n\n\n    /**\n     * 程序在内存清理的时候执行\n     * OnTrimMemory是Android在4.0之后加入的一个回调，任何实现了ComponentCallbacks2接口的类都可以重写实现这个回调方法．OnTrimMemory的主要作用就是指导应用程序在不同的情况下进行自身的内存释放，以避免被系统直接杀掉，提高应用程序的用户体验.\n     * <p>\n     * TRIM_MEMORY_COMPLETE (80)：内存不足，并且该进程在后台进程列表最后一个，马上就要被清理\n     * TRIM_MEMORY_MODERATE (60)：内存不足，并且该进程在后台进程列表的中部。\n     * TRIM_MEMORY_BACKGROUND (40)：内存不足，并且该进程是后台进程。\n     * TRIM_MEMORY_UI_HIDDEN (20)：内存不足，并且该进程的UI已经不可见了。（应用程序的所有UI界面被隐藏了，即用户点击了Home键或者Back键导致应用的UI界面不可见）\n     * <p>\n     * 以上4个是4.0增加\n     * TRIM_MEMORY_RUNNING_CRITICAL (15) ：内存不足(后台进程不足3个)，并且该进程优先级比较高，需要清理内存\n     * TRIM_MEMORY_RUNNING_LOW (10) ：内存不足(后台进程不足5个)，并且该进程优先级比较高，需要清理内存\n     * TRIM_MEMORY_RUNNING_MODERATE (5) ：内存不足(后台进程超过5个)，并且该进程优先级比较高，需要清理内存\n     * 以上3个是4.1增加\n     * <p>\n     * 作者：Gracker\n     * 链接：https://www.jianshu.com/p/5b30bae0eb49\n     */\n    @Override\n    public void onTrimMemory(int level) {\n        super.onTrimMemory(level);\n        KLog.e(\"内存onTrimMemory：\" + level);\n        if (level == TRIM_MEMORY_UI_HIDDEN) {\n            Glide.get(this).clearMemory();\n        }\n        Glide.get(this).trimMemory(level);\n    }\n\n    /**\n     * 内存低的时候执行\n     */\n    @Override\n    public void onLowMemory() {\n        super.onLowMemory();\n        KLog.e(\"内存低\");\n        // 清理 Glide 的缓存\n        Glide.get(this).clearMemory();\n    }\n\n\n    private void initCrashReport() {\n        // 腾讯统计，[可选]设置是否打开debug输出，上线时请关闭，Logcat标签为\"MtaSDK\"\n        StatConfig.setDebugEnable(BuildConfig.DEBUG);\n        // 【基础统计API】由于其内部会调用 Looper.prepare() ，不能放在子线程中\n        StatService.registerActivityLifecycleCallbacks(this);\n        // 初始化并启动MTA：启动MTA线程，加载数据库配置信息，初始化环境，同时还对多版本SDK进行冲突检测\n        try {\n            // 第三个参数必须为：com.tencent.stat.common.StatConstants.VERSION\n            StatService.startStatService(this, \"AAI4F2S2LM1U\", StatConstants.VERSION);\n            KLog.d(\"MTA\", \"MTA初始化成功\");\n        } catch (MtaSDkException e) {\n            // MTA初始化失败\n            KLog.d(\"MTA\", \"MTA初始化失败\" + e);\n        }\n        // 开启或禁用java异常捕获，初始化不会带来任何的流量和性能消耗。生效后，会注册DefaultUncaughtExceptionHandler，crash时捕获相关信息，存储在本地并上报。\n        // 可通过添加StatCrashCallback监听Crash发生。\n        StatCrashReporter.getStatCrashReporter(getApplicationContext()).setJavaCrashHandlerStatus(true);\n\n        // 为了保证运营数据的准确性，建议不要在异步线程初始化Bugly。\n        CrashReport.initCrashReport(getApplicationContext(), \"900044326\", BuildConfig.DEBUG);\n        // 官网现在改用以上的方式\n        //  Bugly.init(getApplicationContext(), \"900044326\", BuildConfig.DEBUG);\n        // 在开发测试阶段，可以在初始化Bugly之前通过以下接口把调试设备设置成“开发设备”。\n        CrashReport.setIsDevelopmentDevice(instance, BuildConfig.DEBUG);\n    }\n\n    public String getWebViewBaseUrl() {\n        if (TextUtils.isEmpty(webViewBaseUrl)) {\n            webViewBaseUrl = \"file://\" + getUserConfigPath();\n        }\n        return webViewBaseUrl;\n    }\n\n    private void initVar() {\n        DisplayMetrics outMetrics = getResources().getDisplayMetrics();\n        screenWidth = outMetrics.widthPixels;\n        screenHeight = outMetrics.heightPixels;\n    }\n\n\n\n    public String getGlobalAssetsFilesDir() {\n        return getExternalFilesDir(null) + File.separator + \"assets\" + File.separator;\n    }\n\n    public String getGlobalConfigPath() {\n        return getExternalFilesDir(null) + File.separator + \"config\" + File.separator;\n    }\n\n    public String getUserFilesDir() {\n        if (user == null) {\n            KLog.e(\"用户为空\");\n            Tool.printCallStatck();\n            return getExternalFilesDir(null) + \"/\";\n        }\n        return getExternalFilesDir(null) + File.separator + user.getId();\n    }\n    public String getUserConfigPath() {\n        return getUserFilesDir() + File.separator + \"config\" + File.separator;\n    }\n\n    public String getUserCachePath() {\n        return getUserFilesDir() + File.separator + \"cache\" + File.separator;\n    }\n\n    public String getUserBoxPath() {\n        return getUserFilesDir() + File.separator + \"box\" + File.separator;\n    }\n\n    public String getUserStorePath() {\n        return getUserFilesDir() + File.separator + \"store\" + File.separator;\n    }\n\n\n    public void clearApiData() {\n        getKeyValue().getString(Contract.UID, null);\n        OkGo.getInstance().cancelAll();\n        WorkManager.getInstance(this).cancelAllWork();\n        CoreDB.i().articleDao().clear(App.i().getUser().getId());\n        CoreDB.i().feedDao().clear(App.i().getUser().getId());\n        CoreDB.i().categoryDao().clear(App.i().getUser().getId());\n        CoreDB.i().feedCategoryDao().clear(App.i().getUser().getId());\n        CoreDB.i().userDao().delete(App.i().getUser().getId());\n        FileUtil.deleteHtmlDir(new File(App.i().getUserFilesDir()));\n    }\n\n    private BaseApi api;\n    public User user;\n\n\n    public User getUser() {\n        if (user == null) {\n            String uid = getKeyValue().getString(Contract.UID, null);\n            if (!TextUtils.isEmpty(uid)) {\n                user = CoreDB.i().userDao().getById(uid);\n            }\n        }\n        return user;\n    }\n\n    public CorePref getKeyValue() {\n        return CorePref.i();\n    }\n\n    public AuthApi getAuthApi() {\n        return (AuthApi) getApi();\n    }\n\n    public OAuthApi getOAuthApi() {\n        return (OAuthApi) getApi();\n    }\n\n    public void setApi(BaseApi baseApi) {\n        this.api = baseApi;\n    }\n\n    public BaseApi getApi() {\n        if (api == null) {\n            switch (getUser().getSource()) {\n                case Contract.PROVIDER_TINYRSS:\n                    TinyRSSApi tinyRSSApi = new TinyRSSApi();\n                    tinyRSSApi.setAuthorization(getUser().getAuth());\n                    api = tinyRSSApi;\n                    break;\n                case Contract.PROVIDER_LOREAD:\n                    LoreadApi loreadApi = new LoreadApi();\n                    loreadApi.setAuthorization(getUser().getAuth());\n                    api = loreadApi;\n                    break;\n                case Contract.PROVIDER_INOREADER:\n                    InoReaderApi inoReaderApi = new InoReaderApi();\n                    inoReaderApi.setAuthorization(getUser().getAuth());\n                    api = inoReaderApi;\n                    break;\n                case Contract.PROVIDER_FEEDLY:\n                    FeedlyApi feedlyApi = new FeedlyApi();\n                    feedlyApi.setAuthorization(getUser().getAuth());\n                    api = feedlyApi;\n                    break;\n                case Contract.PROVIDER_LOCALRSS:\n                    break;\n            }\n            KLog.i(\"初始化 \" + getUser().getSource() + \" = \" + App.i().getUser().getAuth());\n        }\n        return api;\n    }\n\n    public void restartApp() {\n        Intent intent = new Intent(this, SplashActivity.class);\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n        startActivity(intent);\n        android.os.Process.killProcess(android.os.Process.myPid());\n    }\n\n\n    //    //  内存泄漏检测工具\n    //    private void initLeakCanary() {\n    ////        if (LeakCanary.isInAnalyzerProcess(this)) {\n    ////            return;\n    ////        }\n    //        LeakCanary.install(this);\n    //    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/Contract.java",
    "content": "package me.wizos.loread;\n\n/**\n * Created by Wizos on 2019/2/8.\n */\n\npublic class Contract {\n    public static final String PROVIDER_LOCALRSS = \"LocalRSS\";\n    public static final String PROVIDER_INOREADER = \"InoReader\";\n    public static final String PROVIDER_FEEDLY = \"Feedly\";\n    public static final String PROVIDER_TINYRSS = \"TinyTinyRSS\";\n    public static final String PROVIDER_LOREAD = \"Loread\";\n    public static final String UID = \"UID\";\n    public static final String SCHEMA_HTTP = \"http://\";\n    public static final String SCHEMA_HTTPS = \"https://\";\n    public static final String SCHEMA_FILE = \"file://\";\n    public static final String SCHEMA_LOREAD = \"loread://\";\n\n    public static final String HTTP = \"http\";\n    public static final String HTTPS = \"https\";\n\n    public static final String isPortrait = \"isPortrait\";\n    // ACCOUNT_TYPE用于我们当前APP获取系统帐户的唯一标识，这个在account_preferences.xml中有，两处的声明必须是一致\n    // public static final String ACCOUNT_TYPE = \"me.wizos.loreadx\";\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/ArticleActivity.java",
    "content": "package me.wizos.loread.activity;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.ClipData;\nimport android.content.ClipboardManager;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.MutableContextWrapper;\nimport android.content.pm.PackageManager;\nimport android.content.res.Configuration;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Color;\nimport android.net.Uri;\nimport android.net.http.SslError;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.text.InputType;\nimport android.text.TextUtils;\nimport android.view.KeyEvent;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewConfiguration;\nimport android.webkit.JavascriptInterface;\nimport android.webkit.SslErrorHandler;\nimport android.webkit.WebChromeClient;\nimport android.webkit.WebResourceRequest;\nimport android.webkit.WebResourceResponse;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\nimport android.widget.FrameLayout;\nimport android.widget.ProgressBar;\nimport android.widget.RelativeLayout;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.widget.Toolbar;\n\nimport com.afollestad.materialdialogs.DialogAction;\nimport com.afollestad.materialdialogs.GravityEnum;\nimport com.afollestad.materialdialogs.MaterialDialog;\nimport com.afollestad.materialdialogs.Theme;\nimport com.carlt.networklibs.NetType;\nimport com.carlt.networklibs.utils.NetworkUtils;\nimport com.hjq.toast.ToastUtils;\nimport com.lzy.okgo.OkGo;\nimport com.lzy.okgo.callback.FileCallback;\nimport com.lzy.okgo.model.Response;\nimport com.lzy.okgo.request.base.Request;\nimport com.socks.library.KLog;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.jsoup.Jsoup;\nimport org.jsoup.nodes.Document;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.lang.ref.WeakReference;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport cc.shinichi.library.ImagePreview;\nimport cc.shinichi.library.view.listener.OnBigImageLongClickListener;\nimport me.wizos.loread.App;\nimport me.wizos.loread.BuildConfig;\nimport me.wizos.loread.R;\nimport me.wizos.loread.bridge.ArticleBridge;\nimport me.wizos.loread.config.AdBlock;\nimport me.wizos.loread.config.ArticleTags;\nimport me.wizos.loread.config.LinkRewriteConfig;\nimport me.wizos.loread.config.NetworkRefererConfig;\nimport me.wizos.loread.config.SaveDirectory;\nimport me.wizos.loread.config.TestConfig;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.db.ArticleTag;\nimport me.wizos.loread.db.Category;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.Feed;\nimport me.wizos.loread.db.Tag;\nimport me.wizos.loread.network.HttpClientManager;\nimport me.wizos.loread.network.callback.CallbackX;\nimport me.wizos.loread.utils.ArticleUtil;\nimport me.wizos.loread.utils.EncryptUtil;\nimport me.wizos.loread.utils.FileUtil;\nimport me.wizos.loread.utils.ImageUtil;\nimport me.wizos.loread.utils.ScreenUtil;\nimport me.wizos.loread.utils.SnackbarUtil;\nimport me.wizos.loread.utils.StringUtils;\nimport me.wizos.loread.utils.UriUtil;\nimport me.wizos.loread.view.IconFontView;\nimport me.wizos.loread.view.SwipeRefreshLayoutS;\nimport me.wizos.loread.view.WebViewS;\nimport me.wizos.loread.view.colorful.Colorful;\nimport me.wizos.loread.view.slideback.SlideBack;\nimport me.wizos.loread.view.slideback.SlideLayout;\nimport me.wizos.loread.view.slideback.callback.SlideCallBack;\nimport me.wizos.loread.view.webview.DownloadListenerS;\nimport me.wizos.loread.view.webview.LongClickPopWindow;\nimport me.wizos.loread.view.webview.SlowlyProgressBar;\nimport me.wizos.loread.view.webview.VideoImpl;\nimport okhttp3.Call;\nimport okhttp3.Callback;\nimport okhttp3.MediaType;\nimport okhttp3.OkHttpClient;\nimport top.zibin.luban.CompressionPredicate;\nimport top.zibin.luban.InputStreamProvider;\nimport top.zibin.luban.Luban;\nimport top.zibin.luban.OnCompressListener;\nimport top.zibin.luban.OnRenameListener;\n\nimport static me.wizos.loread.Contract.SCHEMA_FILE;\nimport static me.wizos.loread.Contract.SCHEMA_HTTP;\nimport static me.wizos.loread.Contract.SCHEMA_HTTPS;\n\n\n/**\n * @author Wizos on 2017\n */\n@SuppressWarnings(\"unchecked\")\n@SuppressLint(\"SetJavaScriptEnabled\")\npublic class ArticleActivity extends BaseActivity implements ArticleBridge {\n    private static final String TAG = \"ArticleActivity\";\n    private SwipeRefreshLayoutS swipeRefreshLayoutS;\n    private SlowlyProgressBar slowlyProgressBar;\n    private IconFontView starView, readView, saveView, readabilityView;\n    private WebViewS selectedWebView;\n    // private MirrorSwipeBackLayout entryView;\n    // private RefreshLayout entryView;\n    private FrameLayout entryView;\n    private SlideLayout slideLayout;\n    private Toolbar toolbar;\n    private RelativeLayout bottomBar;\n    private VideoImpl video;\n\n    private Article selectedArticle;\n    private int articleNo;\n    private String articleId;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_article);\n        Bundle bundle;\n        if (savedInstanceState != null) {\n            bundle = savedInstanceState;\n            App.i().articleProgress.put(bundle.getString(\"articleId\"), bundle.getInt(\"articleProgress\"));\n        } else {\n            bundle = getIntent().getExtras();\n        }\n        // setSelection 没有滚动效果，直接跳到指定位置。smoothScrollToPosition 有滚动效果的\n        // 文章在列表中的位置编号，下标从 0 开始\n        articleNo = bundle.getInt(\"articleNo\");\n        // 列表中所有的文章数目\n        // articleCount = bundle.getInt(\"articleCount\");\n        articleId = bundle.getString(\"articleId\");\n\n        // KLog.e(\"开始初始化数据2\" + articleNo + \"==\" + articleCount + \"==\" + articleId + \" == \" + articleIDs );\n        initToolbar();\n        initView(); // 初始化界面上的 View，将变量映射到布局上。\n        initSelectedPage(articleNo);\n        imgHttpClient = HttpClientManager.i().imageHttpClient();\n    }\n\n    public static Handler articleHandler = new Handler();\n\n    @Override\n    public void onResume() {\n        selectedWebView.onResume();\n        super.onResume();\n    }\n\n    @Override\n    public void onPause() {\n        selectedWebView.onPause();\n        super.onPause();\n    }\n\n    @Override\n    protected void onDestroy() {\n        saveArticleProgress();\n        OkGo.cancelAll(imgHttpClient);\n\n        // KLog.e(\"onDestroy：\" + selectedWebView);\n        // 如果参数为null的话，会将所有的Callbacks和Messages全部清除掉。\n        // 这样做的好处是在 Acticity 退出的时候，可以避免内存泄露。因为 handler 内可能引用 Activity ，导致 Activity 退出后，内存泄漏。\n        articleHandler.removeCallbacksAndMessages(null);\n        entryView.removeAllViews();\n        selectedWebView.destroy();\n        selectedWebView = null;\n        super.onDestroy();\n    }\n\n    public int saveArticleProgress() {\n        if (selectedWebView == null) {\n            return 0;\n        }\n        int scrollY = selectedWebView.getScrollY();\n        App.i().articleProgress.put(articleId, scrollY);\n        return scrollY;\n    }\n\n    @Override\n    protected void onSaveInstanceState(Bundle outState) {\n        outState.putInt(\"articleNo\", articleNo);\n        outState.putInt(\"articleCount\", 1);\n        outState.putString(\"articleId\", articleId);\n        outState.putInt(\"articleProgress\", saveArticleProgress());\n        outState.putInt(\"theme\", App.i().getUser().getThemeMode());\n        //KLog.i(\"自动保存：\" + articleNo + \"==\" + \"==\" + articleId);\n        super.onSaveInstanceState(outState);\n    }\n\n\n    @JavascriptInterface\n    @Override\n    public void log(String paramString) {\n        KLog.e(ArticleBridge.TAG, \"【log】\" + paramString);\n    }\n\n    @JavascriptInterface\n    @Override\n    public void readImage(String articleId, String imgId, String originalUrl) {\n        String idInMD5 = EncryptUtil.MD5(articleId);\n        String cacheUrl = FileUtil.readCacheFilePath(idInMD5, originalUrl);\n        articleHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                if (TextUtils.isEmpty(cacheUrl)) {\n                    if (!NetworkUtils.isAvailable()) {\n                        selectedWebView.loadUrl(\"javascript:setTimeout( onImageLoadFailed('\" + imgId + \"'),1 )\");\n                    } else if (App.i().getUser().isDownloadImgOnlyWifi() && !NetworkUtils.getNetType().equals(NetType.WIFI)) {\n                        selectedWebView.loadUrl(\"javascript:setTimeout( onImageLoadNeedClick('\" + imgId + \"'),1 )\");\n                    }else {\n                        selectedWebView.loadUrl(\"javascript:setTimeout( onImageLoading('\" + imgId + \"'),1 )\");\n                        downImage(articleId, imgId, originalUrl, false);\n                    }\n                }else {\n                    if(ImageUtil.isImg(new File(cacheUrl))){\n                        selectedWebView.loadUrl(\"javascript:setTimeout( onImageLoadSuccess('\" + imgId + \"','\" + cacheUrl + \"'),1)\");\n                    }else {\n                        selectedWebView.loadUrl(\"javascript:setTimeout( onImageError('\" + imgId + \"'),1 )\");\n                        KLog.e(\"加载图片\", \"缓存文件读取失败：不是图片\");\n                    }\n                }\n            }\n        });\n    }\n\n    @JavascriptInterface\n    @Override\n    public void openImage(String articleId, String imageFilePath) {\n        KLog.e(ArticleBridge.TAG, \"打开图片：\" + this.getPackageName() + \" , \" + imageFilePath + \"  \" );\n        // 如果是 svg 格式的图片则点击无反应\n        if(imageFilePath.endsWith(\".svg\")){\n            return;\n        }\n\n        // 如果传入的是缩略图的文件地址，则替换为大图的\n        Pattern P_COMPRESSED = Pattern.compile(this.getPackageName() + \"/files/(.*?)/compressed/\", Pattern.CASE_INSENSITIVE);\n        Matcher m = P_COMPRESSED.matcher(imageFilePath);\n        if (m.find()) {\n            String id = m.group(1);\n            imageFilePath = m.replaceFirst(this.getPackageName() + \"/files/\" + id + \"/original/\");\n        }\n\n        final String imgUri = imageFilePath;\n\n        ImagePreview.getInstance()\n                // 上下文，必须是activity，不需要担心内存泄漏，本框架已经处理好；\n                .setContext(ArticleActivity.this)\n                // 设置从第几张开始看（索引从0开始）\n                .setIndex(0)\n                //=================================================================================================\n                // 有三种设置数据集合的方式，根据自己的需求进行三选一：\n                // 1：第一步生成的imageInfo List\n                // .setImageInfoList(imageInfoList)\n                // 2：直接传url List\n                //.setImageList(List<String> imageList)\n                // 3：只有一张图片的情况，可以直接传入这张图片的url\n                .setImage(imgUri)\n\n                // 保存的文件夹名称，会在SD卡根目录进行文件夹的新建。\n                // (你也可设置嵌套模式，比如：\"BigImageView/Download\"，会在SD卡根目录新建BigImageView文件夹，并在BigImageView文件夹中新建Download文件夹)\n                .setFolderName(getString(R.string.app_name))\n                .setLoadStrategy(ImagePreview.LoadStrategy.AlwaysOrigin)\n                // 缩放动画时长，单位ms\n//                .setZoomTransitionDuration(300)\n                // 是否启用上拉/下拉关闭。默认不启用\n                .setEnableDragClose(true)\n                // 长按回调\n                .setBigImageLongClickListener(new OnBigImageLongClickListener() {\n                    @Override\n                    public boolean onLongClick(Activity activity, View view, int position) {\n                        Intent shareIntent = new Intent();\n                        shareIntent.setAction(Intent.ACTION_SEND);\n                        shareIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse(imgUri));\n                        shareIntent.setType(\"image/*\");\n\n                        //shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_img));\n                        //shareIntent.putExtra(Intent.EXTRA_TEXT,getString(R.string.share_img));\n                        //shareIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n\n                        startActivity(Intent.createChooser(shareIntent, getString(R.string.share_img)));\n                        return false;\n                    }\n                })\n                // 开启预览\n                .start();\n    }\n\n    private static class MyCompressionPredicate implements CompressionPredicate {\n        @Override\n        public boolean apply(String preCompressedPath, InputStreamProvider path) {\n//         KLog.e(\"检测是否要压缩图片：\" + preCompressedPath);\n            try {\n                if (preCompressedPath.toLowerCase().endsWith(\".gif\")) {\n                    BitmapFactory.Options options = new BitmapFactory.Options();\n                    options.inSampleSize = 1;\n                    options.inJustDecodeBounds = true;\n                    BitmapFactory.decodeStream(path.open(), null, options);\n//                   KLog.e(\"压缩图片，忽略压缩：\" + preCompressedPath + options.outWidth );\n                    return options.outWidth >= 300 || options.outHeight >= 300;\n                } else {\n                    return true;\n                }\n            } catch (Exception e) {\n                e.printStackTrace();\n                return false;\n            }\n        }\n    }\n\n    private static class DownFileCallback extends FileCallback {\n        private WeakReference<Context> weakReferenceContext;\n        private WeakReference<WebViewS> selectedWebView;\n        private String originalFileDir;\n        private String fileNameExt;\n        private String compressedFileDir;\n        private String imgId;\n\n        private String imageUrl;\n        private String articleUrl;\n        private boolean guessReferer;\n\n        DownFileCallback(String destFileDir, String destFileName) {\n            super(destFileDir, destFileName);\n            this.originalFileDir = destFileDir;\n            this.imgId = destFileName;\n        }\n\n        void setParam(Context context, WebViewS webView, String compressedFileDir, String fileNameExt, boolean guessReferer) {\n            this.weakReferenceContext = new WeakReference<Context>(context);\n            this.selectedWebView = new WeakReference<WebViewS>(webView);\n            this.compressedFileDir = compressedFileDir;\n            this.fileNameExt = fileNameExt;\n            this.guessReferer = guessReferer;\n        }\n\n        void setRefererParam(String originalUrl, String articleUrl, boolean guessReferer) {\n            this.imageUrl = originalUrl;\n            this.articleUrl = articleUrl;\n            this.guessReferer = guessReferer;\n        }\n\n        @Override\n        public void onSuccess(Response<File> response) {\n            MediaType mediaType = response.getRawResponse().body().contentType();\n            if( mediaType != null ){\n                if( mediaType.subtype().contains(\"svg\") && !fileNameExt.endsWith(\".svg\")){\n                    fileNameExt = fileNameExt + \".svg\";\n                }\n            }\n            File downloadedOriginalFile = response.body();\n            if(!ImageUtil.isImg(downloadedOriginalFile)){\n                downloadedOriginalFile.delete();\n                if (selectedWebView.get() != null) {\n                    selectedWebView.get().loadUrl(\"javascript:setTimeout( onImageError('\" + imgId + \"'),1)\");\n                }\n                return;\n            }else if(guessReferer){ // 当是根据系统自动猜得的referer而成功下载到图片时，保存自动识别的refer而规则\n                NetworkRefererConfig.i().addReferer(imageUrl, articleUrl);\n            }\n\n\n            File targetOriginalFile = new File(originalFileDir + fileNameExt);\n\n            // 可能存在图片的文件名相同，但是实际是不同图片的情况。\n            if(targetOriginalFile.exists() && downloadedOriginalFile.length() != downloadedOriginalFile.length()){\n                fileNameExt = imgId + \"_\" + fileNameExt;\n                targetOriginalFile = new File(originalFileDir + fileNameExt);\n            }\n            downloadedOriginalFile.renameTo(targetOriginalFile);\n\n            final File finalTargetFile = targetOriginalFile;\n\n            if (selectedWebView.get() == null) {\n                return;\n            }\n//            KLog.i(\"下载图片成功，准备压缩：\" + originalFileDir + prefix + fileNameExt + svgExt  + \"，\" + originalUrl );\n\n            Luban.with(App.i())\n                    .load(targetOriginalFile)\n                    .ignoreBy(100) // 忽略100kb以下的文件\n                    // 缓存压缩图片路径\n                    // .setTargetPath(compressedFileDir + fileNameExt)\n                    .setTargetDir(compressedFileDir)\n                    .setMaxSiz(App.i().screenWidth, App.i().screenHeight)\n                    // 设置开启压缩条件。当路径为空或者为gif时，不压缩\n                    // 压缩后会改变文件地址，所以改回来\n                    .setRenameListener(new OnRenameListener() {\n                        @Override\n                        public String rename(String filePath) {\n                            return fileNameExt;\n                        }\n                    })\n                    .filter(new MyCompressionPredicate())\n                    .setCompressListener(new OnCompressListener() {\n                        @Override\n                        public void onStart() {\n                        }\n\n                        @Override\n                        public void onUnChange(final File file) {\n//                            KLog.e(\"没有压缩图片：\" + Thread.currentThread() + \"   \" + file.getPath() + \"   \" + compressedFileDir);\n                            articleHandler.post(new Runnable() {\n                                @Override\n                                public void run() {\n                                    if (selectedWebView.get() != null) {\n                                        selectedWebView.get().loadUrl(\"javascript:setTimeout( onImageLoadSuccess('\" + imgId + \"','\" + file.getPath() + \"'),1 )\");\n                                    }\n                                }\n                            });\n                        }\n\n                        @Override\n                        public void onSuccess(final File file) {\n                            ImageUtil.mergeBitmap(weakReferenceContext, file, new ImageUtil.OnMergeListener() {\n                                @Override\n                                public void onSuccess() {\n//                                    KLog.e(\"图片合成成功\" + Thread.currentThread());\n                                    articleHandler.post(new Runnable() {\n                                        @Override\n                                        public void run() {\n                                            if (selectedWebView.get() != null) {\n                                                selectedWebView.get().loadUrl(\"javascript:setTimeout( onImageLoadSuccess('\" + imgId + \"','\" + file.getPath() + \"'),1 )\");\n                                            }\n                                        }\n                                    });\n                                }\n\n                                @Override\n                                public void onError(Throwable e) {\n                                    KLog.e(\"合成图片报错：\" + Thread.currentThread());\n                                    e.printStackTrace();\n                                    articleHandler.post(new Runnable() {\n                                        @Override\n                                        public void run() {\n                                            if (selectedWebView.get() != null) {\n                                                selectedWebView.get().loadUrl(\"javascript:setTimeout( onImageLoadSuccess('\" + imgId + \"','\" + finalTargetFile.getPath() + \"'),1)\");\n                                            }\n                                         }\n                                    });\n                                }\n                            });\n                        }\n\n                        @Override\n                        public void onError(Throwable e) {\n                            KLog.e(\"压缩图片报错\" + Thread.currentThread() );\n//                                selectedWebView.loadUrl(\"javascript:onImageLoadSuccess('\" + originalUrl + \"','\" + originalFileDir + fileNameExt + \"')\");\n                            articleHandler.post(new Runnable() {\n                                @Override\n                                public void run() {\n                                    if (selectedWebView.get() != null) {\n                                        selectedWebView.get().loadUrl(\"javascript:setTimeout( onImageLoadSuccess('\" + imgId + \"','\" + finalTargetFile.getPath() + \"'),1)\");\n                                    }\n                                }\n                            });\n                        }\n                    }).launch();\n        }\n\n        // 该方法执行在主线程中\n        @Override\n        public void onError(Response<File> response) {\n            new File(originalFileDir + imgId).delete();\n            KLog.e(\"下载图片失败：\" + imageUrl + \"','\" + response.code() + \"  \" + response.getException());\n            articleHandler.post(new Runnable() {\n                @Override\n                public void run() {\n                    if (selectedWebView.get() != null) {\n                        selectedWebView.get().loadUrl(\"javascript:setTimeout( onImageLoadFailed('\" + imgId + \"'),1)\");\n                    }\n                }\n            });\n        }\n    }\n\n\n    @JavascriptInterface\n    @Override\n    public void downImage(String articleId, String imgId, String originalUrl, boolean guessReferer) {\n        String articleIdInMD5 = EncryptUtil.MD5(articleId);\n        String originalFileDir = App.i().getUserCachePath() + articleIdInMD5 + \"/original/\";\n        String fileNameExt = UriUtil.guessFileNameExt(originalUrl);\n\n        // 下载时的过渡名称为 imgId\n        if (new File(originalFileDir + imgId).exists() || new File(originalFileDir + fileNameExt).exists()) {\n            return;\n        }\n\n        String compressedFileDir = App.i().getUserCachePath() + articleIdInMD5 + \"/compressed/\";\n        DownFileCallback fileCallback = new DownFileCallback(originalFileDir, imgId);\n        fileCallback.setParam(App.i(), selectedWebView, compressedFileDir, fileNameExt, guessReferer);\n        fileCallback.setRefererParam(originalUrl, selectedArticle.getLink(), guessReferer);\n\n        Request request = OkGo.<File>get(originalUrl)\n                .tag(articleId)\n                .client(imgHttpClient);\n\n        if( guessReferer ){\n            request.headers(\"referer\", selectedArticle.getLink());\n            KLog.i(\"来源策略\", \"图片策略：\" + true);\n        }else {\n            String referer = NetworkRefererConfig.i().guessRefererByUrl(originalUrl);\n            if (!StringUtils.isEmpty(referer)) {\n                request.headers(\"referer\", referer);\n                KLog.i(\"来源策略\", \"来源：\" + referer);\n            }\n        }\n\n        request.execute(fileCallback);\n        KLog.e(\"下载图片：\" + originalUrl);\n    }\n\n    @JavascriptInterface\n    @Override\n    public void openLink(String url) {\n        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));\n        // 使用内置浏览器\n        if( !App.i().getUser().isOpenLinkBySysBrowser() && (url.startsWith(SCHEMA_HTTP) || url.startsWith(SCHEMA_HTTPS)) && useInnerBrowser(intent) ){\n            intent = new Intent(ArticleActivity.this, WebActivity.class);\n            intent.setData(Uri.parse(url));\n            intent.putExtra(\"theme\", App.i().getUser().getThemeMode());\n        }\n        //intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n        //intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED );\n        // 添加这一句表示对目标应用临时授权该Uri所代表的文件\n        // intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);\n        startActivity(intent);\n        overridePendingTransition(R.anim.fade_in, R.anim.fade_out);\n    }\n\n    private boolean useInnerBrowser(Intent intent){\n        return getMatchActivitiesSize(intent) == getMatchActivitiesSize(new Intent(Intent.ACTION_VIEW, Uri.parse(\"https://wizos.me\")));\n    }\n    private int getMatchActivitiesSize(Intent intent){\n        return getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size();\n    }\n\n    @JavascriptInterface\n    @Override\n    public void openAudio(String url) {\n        Intent intent = new Intent(this, MusicActivity.class);\n        intent.putExtra(\"title\", selectedArticle.getTitle());\n        intent.setData(Uri.parse(url));\n        startActivity(intent);\n    }\n\n    @JavascriptInterface\n    @Override\n    public void readability() {\n        articleHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                onReadabilityClick();\n            }\n        });\n    }\n\n    private void initView() {\n        starView = findViewById(R.id.article_bottombar_star);\n        starView.setOnLongClickListener(new View.OnLongClickListener() {\n            @Override\n            public boolean onLongClick(View v) {\n                editFavorites(App.i().getUser().getId());\n                return true;\n            }\n        });\n        readView = findViewById(R.id.article_bottombar_read);\n        saveView = findViewById(R.id.article_bottombar_save);\n        swipeRefreshLayoutS = findViewById(R.id.art_swipe_refresh);\n        swipeRefreshLayoutS.setEnabled(false);\n        if (BuildConfig.DEBUG) {\n            saveView.setVisibility(View.VISIBLE);\n            saveView.setOnClickListener(new View.OnClickListener() {\n                @Override\n                public void onClick(View view) {\n                    onClickSaveIcon(view);\n                }\n            });\n        }\n        entryView = findViewById(R.id.slide_arrow_layout);\n        slideLayout = findViewById(R.id.art_slide_layout);\n\n        // KLog.e(\"子数量\" + slideLayout.getChildCount() );\n\n        int color;\n        if (App.i().getUser().getThemeMode() == App.THEME_DAY) {\n            color = Color.BLACK;\n        } else {\n            color = Color.WHITE;\n        }\n        slideLayout.edgeMode(SlideBack.EDGE_BOTH).arrowColor(color).callBack(new SlideCallBack() {\n            @Override\n            public void onSlide(int edgeFrom) {\n                if (edgeFrom == SlideBack.EDGE_LEFT) {\n                    onLeftBack();\n                    entryView.scrollBy(0, 0);\n                } else {\n                    onRightBack();\n                    entryView.scrollBy(0, 0);\n                }\n            }\n\n            @Override\n            public void onViewSlide(int edgeFrom, int offset) {\n                //KLog.e(\"拖动方向：\" + edgeFrom + \" , \" + offset);\n                if (edgeFrom == SlideBack.EDGE_LEFT) {\n                    entryView.scrollTo(-offset, 0);\n                } else {\n                    entryView.scrollTo(offset, 0);\n                }\n            }\n        }).register();\n    }\n\n    private void initToolbar() {\n        bottomBar = findViewById(R.id.art_bottombar);\n        toolbar = findViewById(R.id.art_toolbar);\n        setSupportActionBar(toolbar);\n        // 这个小于4.0版本是默认为true，在4.0及其以上是false。该方法的作用：决定左上角的图标是否可以点击(没有向左的小图标)，true 可点\n        getSupportActionBar().setHomeButtonEnabled(true);\n        // 决定左上角图标的左侧是否有向左的小箭头，true 有小箭头\n        getSupportActionBar().setDisplayHomeAsUpEnabled(true);\n        getSupportActionBar().setDisplayShowTitleEnabled(false);\n        slowlyProgressBar = new SlowlyProgressBar((ProgressBar) findViewById(R.id.article_progress_bar));\n        toolbar.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View view) {\n                if (articleHandler.hasMessages(App.MSG_DOUBLE_TAP) && selectedWebView != null) {\n                    articleHandler.removeMessages(App.MSG_DOUBLE_TAP);\n                    selectedWebView.scrollTo(0, 0);\n                } else {\n                    articleHandler.sendEmptyMessageDelayed(App.MSG_DOUBLE_TAP, ViewConfiguration.getDoubleTapTimeout());\n                }\n            }\n        });\n    }\n\n    public void onLeftBack() {\n        if (App.i().articlesAdapter == null || articleNo - 1 < 0) {\n            ToastUtils.show(\"没有文章了\");\n            return;\n        }\n        saveArticleProgress();\n        articleNo = articleNo - 1;\n        initSelectedPage(articleNo);\n    }\n\n    public void onRightBack() {\n        if (App.i().articlesAdapter == null || articleNo + 1 >= App.i().articlesAdapter.getItemCount()) {\n            ToastUtils.show(\"没有文章了\");\n            return;\n        }\n        saveArticleProgress();\n        articleNo = articleNo + 1;\n        initSelectedPage(articleNo);\n    }\n\n    public void initSelectedPage(int position) {\n        swipeRefreshLayoutS.setRefreshing(false);\n        reInitSelectedArticle(position);\n        initSelectedWebViewContent();\n    }\n\n\n    public void reInitSelectedArticle(int position) {\n        // 取消之前那篇文章的图片下载(但是如果回到之前那篇文章，怎么恢复下载呢？)\n        OkGo.cancelTag(imgHttpClient, articleId);\n        articleNo = position;\n        if (App.i().articlesAdapter != null && position < App.i().articlesAdapter.getItemCount()) {\n            selectedArticle = App.i().articlesAdapter.getItem(position);\n//            selectedArticle = CoreDB.i().articleDao().getById(App.i().getUser().getId(),App.i().articlesAdapter.getArticleId(position));\n            articleId = selectedArticle.getId();\n        } else {\n            selectedArticle = CoreDB.i().articleDao().getById(App.i().getUser().getId(), articleId);\n        }\n        initIconState();\n        initFeedConfig();\n    }\n\n    private int downX, downY;\n\n    // （webview在实例化后，可能还在渲染html，不一定能执行js）\n    @SuppressLint(\"ClickableViewAccessibility\")\n    private void initSelectedWebViewContent() {\n        if (selectedWebView == null) {\n            selectedWebView = new WebViewS(new MutableContextWrapper(App.i()));\n            entryView.removeAllViews();\n            entryView.addView(selectedWebView);\n            // 初始化视频处理类\n            video = new VideoImpl(ArticleActivity.this, selectedWebView);\n            selectedWebView.setWebChromeClient(new WebChromeClientX(video, new WeakReference<SlowlyProgressBar>(slowlyProgressBar)));\n            selectedWebView.setWebViewClient(new WebViewClientX());\n            // 原本想放在选择 webview 页面的时候去加载，但可能由于那时页面内容已经加载所以无法设置下面这个JSInterface？\n            selectedWebView.addJavascriptInterface(ArticleActivity.this, ArticleBridge.TAG);\n            selectedWebView.setDownloadListener(new DownloadListenerS(this));\n            selectedWebView.setOnTouchListener(new View.OnTouchListener() {\n                @Override\n                public boolean onTouch(View arg0, MotionEvent arg1) {\n                    downX = (int) arg1.getX();\n                    downY = (int) arg1.getY();\n                    return false;\n                }\n            });\n\n            // 作者：Wing_Li，链接：https://www.jianshu.com/p/3fcf8ba18d7f\n            selectedWebView.setOnLongClickListener(new View.OnLongClickListener() {\n                @Override\n                public boolean onLongClick(View webView) {\n                    WebView.HitTestResult result = ((WebView) webView).getHitTestResult();\n                    if (null == result) {\n                        return false;\n                    }\n                    int type = result.getType();\n                    if (type == WebView.HitTestResult.UNKNOWN_TYPE) {\n                        return false;\n                    }\n\n                    // 这里可以拦截很多类型，我们只处理超链接就可以了\n                    new LongClickPopWindow(ArticleActivity.this, (WebView) webView, ScreenUtil.dp2px(ArticleActivity.this, 120), ScreenUtil.dp2px(ArticleActivity.this, 130), downX, downY + 10);\n//                    webViewLongClickedPopWindow.showAtLocation(webView, Gravity.TOP|Gravity.LEFT, downX, downY + 10);\n                    return true;\n                }\n            });\n        }\n\n        // 检查该订阅源默认显示什么。【RSS，已读，保存的网页，原始网页】\n        // KLog.e(\"要加载的位置为：\" + position + \"  \" + selectedArticle.getTitle());\n        Feed feed = CoreDB.i().feedDao().getById(App.i().getUser().getId(), selectedArticle.getFeedId());\n        if (feed != null) {\n            toolbar.setTitle(feed.getTitle());\n            if (App.DISPLAY_LINK.equals(TestConfig.i().getDisplayMode(feed.getId()))) {\n                selectedWebView.loadUrl(selectedArticle.getLink());\n                // 判断是要在加载的时候获取还是同步的时候获取\n            } else {\n//                KLog.e(\"加载文章：\" + selectedArticle.getTitle());\n                selectedWebView.loadData(ArticleUtil.getPageForDisplay(selectedArticle));\n            }\n        } else {\n            selectedWebView.loadData(ArticleUtil.getPageForDisplay(selectedArticle));\n        }\n        selectedWebView.requestFocus();\n    }\n\n\n    private static class WebChromeClientX extends WebChromeClient {\n        VideoImpl video;\n        WeakReference<SlowlyProgressBar> slowlyProgressBar;\n\n        WebChromeClientX(VideoImpl video, WeakReference<SlowlyProgressBar> progressBar) {\n            this.video = video;\n            this.slowlyProgressBar = progressBar;\n        }\n\n        @Override\n        public void onProgressChanged(WebView webView, int progress) {\n            // 增加Javascript异常监控，不能增加，会造成页面卡死\n            // CrashReport.setJavascriptMonitor(webView, true);\n            if (slowlyProgressBar.get() != null) {\n                slowlyProgressBar.get().onProgressChange(progress);\n            }\n        }\n\n        // 表示进入全屏的时候\n        @Override\n        public void onShowCustomView(View view, CustomViewCallback callback) {\n            if (video != null) {\n                video.onShowCustomView(view, callback);\n            }\n        }\n\n        //表示退出全屏的时候\n        @Override\n        public void onHideCustomView() {\n            if (video != null) {\n                video.onHideCustomView();\n            }\n        }\n    }\n\n    private class WebViewClientX extends WebViewClient {\n        // 通过重写WebViewClient的onReceivedSslError方法来接受所有网站的证书，忽略SSL错误。\n        @Override\n        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {\n            KLog.e(\"SSL错误\");\n            handler.proceed(); // 忽略SSL证书错误，继续加载页面\n        }\n\n        @Deprecated\n        @SuppressLint(\"NewApi\")\n        @Override\n        public WebResourceResponse shouldInterceptRequest(WebView view, final WebResourceRequest request) {\n            //KLog.e(\"【请求加载资源】\" + url);\n            if ( AdBlock.i().isAd(request.getUrl().toString()) ) {\n                // 有广告的请求数据，我们直接返回空数据，注：不能直接返回null\n                return new WebResourceResponse(null, null, null);\n            }\n            return super.shouldInterceptRequest(view, request);\n        }\n\n        /**\n         * @param webView\n         * @param url\n         * @return\n         * 返回 true 表示你已经处理此次请求。\n         * 返回 false 表示由webview自行处理（一般都是把此url加载出来）。\n         * 返回super.shouldOverrideUrlLoading(view, url); 这个返回的方法会调用父类方法，也就是跳转至手机浏览器\n         */\n        @Override\n        public boolean shouldOverrideUrlLoading(WebView webView, String url) {\n            KLog.e(\"url为：\" + url);\n            // 判断重定向的方式一\n            // 作者：胡几手，链接：https://www.jianshu.com/p/7dfb8797f893\n            // 解决在webView第一次加载的url重定向到了另一个地址时，也会走shouldOverrideUrlLoading回调的问题\n            WebView.HitTestResult hitTestResult = webView.getHitTestResult();\n            if (hitTestResult == null) {\n                return false;\n            } else if (hitTestResult.getType() == WebView.HitTestResult.UNKNOWN_TYPE) {\n                return false;\n            }\n\n            if (TextUtils.isEmpty(url) || url.startsWith(SCHEMA_FILE)) {\n                return true;\n            }\n\n            String newUrl = LinkRewriteConfig.i().getRedirectUrl( url );\n            if (!TextUtils.isEmpty(newUrl)) {\n                // 创建一个新请求，并相应地修改它\n                url = newUrl;\n            }\n            openLink(url);\n            return true;\n        }\n\n        @Override\n        public void onPageStarted(WebView webView, String url, Bitmap favicon) {\n            super.onPageStarted(webView, url, favicon);\n            //KLog.e(\"页面加载开始\");\n            if (slowlyProgressBar != null) {\n                slowlyProgressBar.onProgressStart();\n            }\n        }\n\n        /**\n         * 不能直接在这里就初始化setupImage，因为在viewpager中预加载而生成webview的时候，这里的懒加载就被触发了\n         * webView.loadUrl(\"javascript:setTimeout(\\\"setupImage()\\\",100)\");\n         */\n        @Override\n        public void onPageFinished(WebView webView, String url) {\n            super.onPageFinished(webView, url);\n            webView.getSettings().setBlockNetworkImage(false);\n            Integer process = App.i().articleProgress.get(articleId);\n            // KLog.e(\"页面加载完成：\" + selectedArticle.getTitle() + \"  \" + articleId + \"  \" + process);\n            if (process != null && selectedWebView != null) {\n                selectedWebView.scrollTo(0, process);\n            }\n        }\n    }\n\n    private void initIconState() {\n        if (selectedArticle.getReadStatus() == App.STATUS_UNREAD) {\n            readView.setText(getString(R.string.font_readed));\n            selectedArticle.setReadStatus(App.STATUS_READED);\n            CoreDB.i().articleDao().update(selectedArticle);\n            App.i().getApi().markArticleReaded(selectedArticle.getId(), new CallbackX() {\n                @Override\n                public void onSuccess(Object result) { }\n\n                @Override\n                public void onFailure(Object error) {\n                    selectedArticle.setReadStatus(App.STATUS_UNREAD);\n                    CoreDB.i().articleDao().update(selectedArticle);\n                    ToastUtils.show(getString(R.string.mask_fail));\n                }\n            });\n        } else if (selectedArticle.getReadStatus() == App.STATUS_READED) {\n            readView.setText(getString(R.string.font_readed));\n        } else if (selectedArticle.getReadStatus() == App.STATUS_UNREADING) {\n            readView.setText(getString(R.string.font_unread));\n        }\n\n        if (selectedArticle.getStarStatus() == App.STATUS_UNSTAR) {\n            starView.setText(getString(R.string.font_unstar));\n        } else {\n            starView.setText(getString(R.string.font_stared));\n        }\n        if (App.STATUS_NOT_FILED == selectedArticle.getSaveStatus()) {\n            saveView.setText(getString(R.string.font_unsave));\n        } else {\n            saveView.setText(getString(R.string.font_saved));\n        }\n    }\n\n\n    public void onReadClick(View view) {\n//        KLog.e(\"loread\", \"被点击的是：\" + selectedArticle.getTitle());\n        if (selectedArticle.getReadStatus() == App.STATUS_READED) {\n            readView.setText(getString(R.string.font_unread));\n            selectedArticle.setReadStatus(App.STATUS_UNREADING);\n            CoreDB.i().articleDao().update(selectedArticle);\n            App.i().getApi().markArticleUnread(selectedArticle.getId(), new CallbackX() {\n                @Override\n                public void onSuccess(Object result) {\n                }\n\n                @Override\n                public void onFailure(Object error) {\n                    selectedArticle.setReadStatus(App.STATUS_READED);\n                    CoreDB.i().articleDao().update(selectedArticle);\n                    ToastUtils.show(getString(R.string.mask_fail));\n                }\n            });\n        } else {\n            readView.setText(getString(R.string.font_readed));\n            selectedArticle.setReadStatus(App.STATUS_READED);\n            CoreDB.i().articleDao().update(selectedArticle);\n\n            App.i().getApi().markArticleReaded(selectedArticle.getId(), new CallbackX() {\n                @Override\n                public void onSuccess(Object result) {\n                }\n\n                @Override\n                public void onFailure(Object error) {\n                    selectedArticle.setReadStatus(App.STATUS_UNREAD);\n                    CoreDB.i().articleDao().update(selectedArticle);\n                    ToastUtils.show(getString(R.string.mask_fail));\n                }\n            });\n\n        }\n    }\n\n\n    public void onClickStarIcon(View view) {\n        String uid = App.i().getUser().getId();\n        if (selectedArticle.getStarStatus() == App.STATUS_UNSTAR) {\n            starView.setText(getString(R.string.font_stared));\n            selectedArticle.setStarStatus(App.STATUS_STARED);\n            CoreDB.i().articleDao().update(selectedArticle);\n            App.i().getApi().markArticleStared(selectedArticle.getId(), new CallbackX() {\n                @Override\n                public void onSuccess(Object result) {\n                }\n\n                @Override\n                public void onFailure(Object error) {\n                    selectedArticle.setStarStatus(App.STATUS_UNSTAR);\n                    CoreDB.i().articleDao().update(selectedArticle);\n                    ToastUtils.show(getString(R.string.mask_fail));\n                }\n            });\n\n            List<Category> categories = CoreDB.i().categoryDao().getByFeedId(uid,selectedArticle.getFeedId());\n            String msg = null;\n            String action = null;\n            if(categories == null || StringUtils.isEmpty(categories)){\n                msg = getString(R.string.star_marked);\n                action = getString(R.string.add_to_favorites);\n            }else if(categories.size() == 1){\n                msg = getString(R.string.star_marked_to_favorites,categories.get(0).getTitle());\n                action = getString(R.string.edit_favorites);\n\n                Tag tag = new Tag();\n                tag.setUid(uid);\n                tag.setId(categories.get(0).getTitle());\n                tag.setTitle(categories.get(0).getTitle());\n                CoreDB.i().tagDao().insert(tag);\n                ArticleTag articleTag = new ArticleTag(uid,selectedArticle.getId(),tag.getId());\n                CoreDB.i().articleTagDao().insert(articleTag);\n            }else {\n                msg = getString(R.string.star_marked_to_favorites,categories.get(0).getTitle() + getString(R.string.etc));\n                action = getString(R.string.edit_favorites);\n\n                Tag tag = new Tag();\n                tag.setUid(uid);\n                tag.setId(categories.get(0).getTitle());\n                //tag.setTitle(categories.get(0).getTitle());\n                CoreDB.i().tagDao().insert(tag);\n                ArticleTag articleTag = new ArticleTag(uid,selectedArticle.getId(),tag.getId());\n                CoreDB.i().articleTagDao().insert(articleTag);\n            }\n\n            SnackbarUtil.Long(swipeRefreshLayoutS, bottomBar, msg).setAction(action, v -> editFavorites(uid)).show();\n        } else {\n            starView.setText(getString(R.string.font_unstar));\n            selectedArticle.setStarStatus(App.STATUS_UNSTAR);\n            CoreDB.i().articleDao().update(selectedArticle);\n            App.i().getApi().markArticleUnstar(selectedArticle.getId(), new CallbackX() {\n                @Override\n                public void onSuccess(Object result) {\n                }\n\n                @Override\n                public void onFailure(Object error) {\n                    selectedArticle.setStarStatus(App.STATUS_STARED);\n                    CoreDB.i().articleDao().update(selectedArticle);\n                    ToastUtils.show(getString(R.string.mask_fail));\n                }\n            });\n            CoreDB.i().articleTagDao().deleteByArticleId(uid,selectedArticle.getId());\n            ArticleTags.i().removeArticle(selectedArticle.getId());\n            ArticleTags.i().save();\n        }\n    }\n\n\n    public void editFavorites(String uid){\n        // 找出当前用户有的所有tags\n        List<Tag> tags = CoreDB.i().tagDao().getAll(uid);\n        // 找出当前用户该文章的tags\n        List<ArticleTag> originalArticleTags = CoreDB.i().articleTagDao().getByArticleId(uid, selectedArticle.getId());\n\n        Integer[] preSelectedIndices = null;\n        int index = 0;\n        if(originalArticleTags!=null && originalArticleTags.size() > 0){\n            preSelectedIndices = new Integer[]{originalArticleTags.size()};\n        }\n        String[] tagTitles;\n        if( tags != null ){\n            tagTitles = new String[tags.size()];\n            for (int i = 0, size = tags.size(); i < size; i++) {\n                String title = tags.get(i).getTitle();\n                tagTitles[i] = title;\n                //KLog.e(\"标题ss：\" + title + \" , \" + originalArticleTags + \"  \" + index + \"  \" + i );\n                if(preSelectedIndices!=null){\n                    for (ArticleTag articleTag:originalArticleTags) {\n                        //KLog.e(\"标题：\" + title + \" , \" + articleTag.getTagId() + \"  \" + index + \"  \" + i );\n                        if(title.equals(articleTag.getTagId())){\n                            preSelectedIndices[index] = i;\n                            index ++;\n                        }\n                    }\n                }\n            }\n\n            ArrayList<ArticleTag> selectedArticleTags = new ArrayList<>();\n            new MaterialDialog.Builder(ArticleActivity.this)\n                    .title(getString(R.string.select_tag))\n                    .items(tagTitles)\n                    .itemsCallbackMultiChoice(preSelectedIndices, (dialog, which, text) -> {\n                        selectedArticleTags.clear();\n                        ArticleTag articleTag;\n                        for (int i : which) {\n                            articleTag = new ArticleTag(uid, selectedArticle.getId(), tags.get(i).getId() );\n                            selectedArticleTags.add(articleTag);\n                        }\n                        KLog.e(\"已选择收藏夹：\" + Arrays.toString(text));\n                        return true;\n                    })\n                    .positiveText(R.string.confirm)\n                    .onPositive(new MaterialDialog.SingleButtonCallback() {\n                        @Override\n                        public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n                            if(selectedArticleTags.size() > 0){\n                                CoreDB.i().articleTagDao().deleteByArticleId(selectedArticle.getUid(),selectedArticle.getId());\n                                CoreDB.i().articleTagDao().insert(selectedArticleTags);\n                                ArticleTags.i().removeArticle(selectedArticle.getId());\n                                ArticleTags.i().addArticleTags(selectedArticleTags);\n                                ArticleTags.i().save();\n                            }\n                            dialog.dismiss();\n                        }\n                    })\n                    .neutralText(getString(R.string.new_favorites))\n                    .onNeutral(new MaterialDialog.SingleButtonCallback() {\n                        @Override\n                        public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n                            newFavorites(uid,dialog);\n                        }\n                    })\n                    .alwaysCallMultiChoiceCallback() // the callback will always be called, to check if selection is still allowed\n                    .show();\n        }else {\n            newFavorites(uid,null);\n        }\n    }\n    public void newFavorites(String uid,@Nullable MaterialDialog lastDialog){\n        new MaterialDialog.Builder(ArticleActivity.this)\n                .title(R.string.new_favorites)\n                .inputType(InputType.TYPE_CLASS_TEXT)\n                .inputRange(1, 16)\n                .input(getString(R.string.new_favorites), null, new MaterialDialog.InputCallback() {\n                    @Override\n                    public void onInput(@NonNull MaterialDialog dialog, CharSequence input) {\n                        Tag tag = new Tag();\n                        tag.setUid(uid);\n                        tag.setId(input.toString());\n                        tag.setTitle(input.toString());\n                        CoreDB.i().tagDao().insert(tag);\n                        dialog.dismiss();\n                        if(lastDialog != null){\n                            lastDialog.dismiss();\n                        }\n                        editFavorites(uid);\n                        KLog.e(\"正在新建收藏夹：\" + input.toString());\n                    }\n                })\n                .positiveText(R.string.confirm)\n                .negativeText(android.R.string.cancel)\n                .show();\n    }\n\n\n\n//    // 找出当前用户有的所有tags\n//    List<Tag> tags = CoreDB.i().tagDao().getAll(uid);\n//    // 找出当前用户该文章的tags\n//    List<ArticleTag> originalArticleTags = CoreDB.i().articleTagDao().getByArticleId(uid, articleId);\n//\n//    String[] tagTitles = new String[0];\n//    Integer[] preSelectedIndices = new Integer[]{0};\n//        if( tags != null ){\n//        tagTitles = new String[tags.size()];\n//\n//        HashSet<String> tagIdSet = new HashSet();\n//        if(originalArticleTags!=null){\n//            preSelectedIndices = new Integer[]{originalArticleTags.size()};\n//            for (int i = 0, size = originalArticleTags.size(); i < size; i++) {\n//                tagIdSet.add(originalArticleTags.get(i).getTagId());\n//            }\n//        }\n//\n//        int index = 0;\n//        for (int i = 0, size = tags.size(); i < size; i++) {\n//            String title = tags.get(i).getTitle();\n//            tagTitles[i] = title;\n//            if(tagIdSet.contains(title)){\n//                preSelectedIndices[index] = i;\n//                index++;\n//            }\n//        }\n//\n//        new MaterialDialog.Builder(ArticleActivity.this)\n//                .title(getString(R.string.select_tag))\n//                .items(tagTitles)\n//                .itemsCallbackMultiChoice(preSelectedIndices, (dialog, which, text) -> {\n//                    final ArrayList<ArticleTag> selectedArticleTags = new ArrayList<>();\n//                    ArticleTag articleTag;\n//                    for (int i : which) {\n//                        articleTag = new ArticleTag(uid, articleId, tags.get(i).getId() );\n//                        selectedArticleTags.add(articleTag);\n//                    }\n//                    CoreDB.i().articleTagDao().delete(originalArticleTags);\n//                    CoreDB.i().articleTagDao().insert(selectedArticleTags);\n//                    return true;\n//                })\n//                .alwaysCallMultiChoiceCallback() // the callback will always be called, to check if selection is still allowed\n//                .show();\n//    }\n\n\n    public void onClickSaveIcon(View view) {\n        if (selectedArticle.getSaveStatus() == App.STATUS_NOT_FILED) {\n            saveView.setText(getString(R.string.font_saved));\n            selectedArticle.setSaveStatus(App.STATUS_TO_BE_FILED);\n            addToSaveDirectory(App.i().getUser().getId());\n        } else if (selectedArticle.getSaveStatus() == App.STATUS_TO_BE_FILED){\n            saveView.setText(getString(R.string.font_unsave));\n            selectedArticle.setSaveStatus(App.STATUS_NOT_FILED);\n            SaveDirectory.i().setArticleDirectory(selectedArticle.getId(),null);\n        } else if (selectedArticle.getSaveStatus() == App.STATUS_IS_FILED){\n            saveView.setText(getString(R.string.font_saved));\n            ToastUtils.show(getString(R.string.is_filed_cannot_edit));\n        }\n        CoreDB.i().articleDao().update(selectedArticle);\n    }\n\n\n    public void clearDirectory(String uid){\n        SaveDirectory.i().setArticleDirectory(selectedArticle.getId(),null);\n    }\n    public void addToSaveDirectory(String uid){\n        String dir = SaveDirectory.i().getSaveDir(selectedArticle.getFeedId(),selectedArticle.getId());\n        String msg;\n        if (StringUtils.isEmpty(dir)) {\n            msg = getString(R.string.saved_to_root_directory);\n        }else {\n            msg = getString(R.string.saved_to_directory,dir);\n        }\n\n        SnackbarUtil.Long(swipeRefreshLayoutS, bottomBar, msg)\n                .setAction(R.string.edit_directory, v -> editDirectory(uid)).show();\n    }\n\n    public void editDirectory(String uid){\n        String[] savedFoldersTitle = SaveDirectory.i().getDirectoriesOptionName();\n        List<String> savedFoldersValue = SaveDirectory.i().getDirectoriesOptionValue();\n        new MaterialDialog.Builder(this)\n                .title(getString(R.string.edit_directory))\n                .items(savedFoldersTitle)\n                .itemsCallbackSingleChoice(-1, new MaterialDialog.ListCallbackSingleChoice() {\n                    @Override\n                    public boolean onSelection(MaterialDialog dialog, View itemView, int which, CharSequence text) {\n                        SaveDirectory.i().setArticleDirectory(selectedArticle.getId(),savedFoldersValue.get(which));\n                        SaveDirectory.i().save();\n                        KLog.e(\"被选择的目录为：\" + text.toString());\n                        return true;\n                    }\n                })\n//                .neutralText(getString(R.string.new_directory))\n//                .onNeutral(new MaterialDialog.SingleButtonCallback() {\n//                    @Override\n//                    public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n//                        newDirectory(uid,dialog);\n//                    }\n//                })\n                .show();\n    }\n//\n//    public void newDirectory(String uid,@Nullable MaterialDialog lastDialog){\n//        new MaterialDialog.Builder(this)\n//                .title(R.string.new_directory)\n//                .inputType(InputType.TYPE_CLASS_TEXT)\n//                .inputRange(1, 16)\n//                .input(getString(R.string.new_directory), null, new MaterialDialog.InputCallback() {\n//                    @Override\n//                    public void onInput(@NonNull MaterialDialog dialog, CharSequence input) {\n//                        SaveDirectory.i().newDirectory(input.toString());\n//                        SaveDirectory.i().save();\n//                        if(lastDialog != null){\n//                            lastDialog.dismiss();\n//                        }\n//                        editDirectory(uid);\n//                        KLog.e(\"正在新建收藏夹：\" + input.toString());\n//                    }\n//                })\n//                .positiveText(R.string.confirm)\n//                .negativeText(android.R.string.cancel)\n//                .show();\n//    }\n\n    public void clickOpenOriginalArticle(View view) {\n        Intent intent = new Intent(ArticleActivity.this, WebActivity.class);\n        intent.setData(Uri.parse(selectedArticle.getLink()));\n        intent.putExtra(\"theme\", App.i().getUser().getThemeMode());\n        startActivity(intent);\n        overridePendingTransition(R.anim.fade_in, R.anim.fade_out);\n    }\n\n    private Article optimizedArticle;\n    public void onReadabilityClick(View view) {\n        if(swipeRefreshLayoutS.isRefreshing()){\n            OkGo.cancelTag(HttpClientManager.i().simpleClient(),\"Readability\");\n            swipeRefreshLayoutS.setRefreshing(false);\n            return;\n        }\n        saveArticleProgress();\n        if(optimizedArticle != null){\n            ToastUtils.show(getString(R.string.cancel_readability));\n            selectedWebView.loadData(ArticleUtil.getPageForDisplay(selectedArticle));\n            CoreDB.i().articleDao().update(selectedArticle);\n            ((IconFontView)view).setText(getString(R.string.font_article_original));\n            optimizedArticle = null;\n        }else {\n            ToastUtils.show(getString(R.string.get_readability_ing));\n            swipeRefreshLayoutS.setRefreshing(true);\n\n            okhttp3.Request request = new okhttp3.Request.Builder().url(selectedArticle.getLink()).tag(\"Readability\").build();\n            Call call = HttpClientManager.i().simpleClient().newCall(request);\n            call.enqueue(new Callback() {\n                @Override\n                public void onFailure(@NotNull Call call, @NotNull IOException e) {\n                    articleHandler.post(new Runnable() {\n                        @Override\n                        public void run() {\n                            if (swipeRefreshLayoutS == null) {\n                                return;\n                            }\n                            swipeRefreshLayoutS.setRefreshing(false);\n                            ToastUtils.show(getString(R.string.get_readability_failure));\n                        }\n                    });\n                }\n\n\n                // 这是因为OkHttp对于异步的处理仅仅是开启了一个线程，并且在线程中处理响应，所以不能再其中操作UI。\n                // OkHttp是一个面向于Java应用而不是特定平台(Android)的框架，那么它就无法在其中使用Android独有的Handler机制。\n                @Override\n                public void onResponse(@NotNull Call call, @NotNull okhttp3.Response response) throws IOException {\n                    optimizedArticle = ArticleUtil.getReadabilityArticle(selectedArticle,response.body());\n                    CoreDB.i().articleDao().update(optimizedArticle);\n                    articleHandler.post(new Runnable() {\n                        @Override\n                        public void run() {\n                            if (swipeRefreshLayoutS == null ||selectedWebView == null) {\n                                return;\n                            }\n                            swipeRefreshLayoutS.setRefreshing(false);\n                            ToastUtils.show(getString(R.string.get_readability_success));\n                            ((IconFontView)view).setText(getString(R.string.font_article_readability));\n                            selectedWebView.loadData(ArticleUtil.getPageForDisplay(optimizedArticle));\n                        }\n                    });\n                }\n            });\n        }\n    }\n\n    public void onReadabilityClick() {\n        saveArticleProgress();\n        if (selectedWebView.isReadability()) {\n            ToastUtils.show(getString(R.string.cancel_readability));\n            selectedWebView.loadData(ArticleUtil.getPageForDisplay(selectedArticle));\n            selectedWebView.setReadability(false);\n            return;\n        }\n\n        swipeRefreshLayoutS.setRefreshing(true);\n\n        ToastUtils.show(getString(R.string.get_readability_ing));\n\n        okhttp3.Request request = new okhttp3.Request.Builder().url(selectedArticle.getLink()).build();\n        Call call = HttpClientManager.i().simpleClient().newCall(request);\n        call.enqueue(new Callback() {\n            @Override\n            public void onFailure(@NotNull Call call, @NotNull IOException e) {\n                articleHandler.post(new Runnable() {\n                    @Override\n                    public void run() {\n                        if (swipeRefreshLayoutS != null) {\n                            swipeRefreshLayoutS.setRefreshing(false);\n                            ToastUtils.show(getString(R.string.get_readability_failure));\n                        }\n                    }\n                });\n            }\n\n\n            // 这是因为OkHttp对于异步的处理仅仅是开启了一个线程，并且在线程中处理响应，所以不能再其中操作UI。\n            // OkHttp是一个面向于Java应用而不是特定平台(Android)的框架，那么它就无法在其中使用Android独有的Handler机制。\n            @Override\n            public void onResponse(@NotNull Call call, @NotNull okhttp3.Response response) throws IOException {\n                if (!response.isSuccessful()) {\n                    if (swipeRefreshLayoutS != null) {\n                        swipeRefreshLayoutS.setRefreshing(false);\n                    }\n                    return;\n                }\n\n                Article optimizedArticle = ArticleUtil.getReadabilityArticle(selectedArticle,response.body());\n\n                articleHandler.post(new Runnable() {\n                    @Override\n                    public void run() {\n                        if (swipeRefreshLayoutS != null) {\n                            swipeRefreshLayoutS.setRefreshing(false);\n                        }\n                        if (selectedWebView == null) {\n                            return;\n                        }\n\n                        selectedWebView.loadData(ArticleUtil.getPageForDisplay(optimizedArticle));\n                        ToastUtils.show(getString(R.string.get_readability_success));\n                        selectedWebView.setReadability(true);\n\n                        SnackbarUtil.Long(swipeRefreshLayoutS, bottomBar, getString(R.string.save_readability_content))\n                                .setAction(getString(R.string.agree), new View.OnClickListener() {\n                                    @Override\n                                    public void onClick(View v) {\n                                        CoreDB.i().articleDao().update(optimizedArticle);\n                                    }\n                                }).show();\n                    }\n                });\n            }\n        });\n    }\n\n    private void initFeedConfig() {\n        final Feed feed = CoreDB.i().feedDao().getById(App.i().getUser().getId(), selectedArticle.getFeedId());\n        //final View feedConfigView = findViewById(R.id.article_feed_config);\n        if( feedMenuItem != null ){\n            if (feed != null) {\n                feedMenuItem.setVisible(true);\n            }else {\n                feedMenuItem.setVisible(false);\n            }\n        }\n    }\n\n\n    public void showArticleInfo() {\n//        KLog.e(\"文章信息\");\n        if (!BuildConfig.DEBUG) {\n            return;\n        }\n        Document document = Jsoup.parseBodyFragment(ArticleUtil.getPageForDisplay(selectedArticle));\n        document.outputSettings().prettyPrint(true);\n        String info = selectedArticle.getTitle() + \"\\n\" +\n                \"ID=\" + selectedArticle.getId() + \"\\n\" +\n                \"ID-MD5=\" + EncryptUtil.MD5(selectedArticle.getId()) + \"\\n\" +\n                \"ReadState=\" + selectedArticle.getReadStatus() + \"\\n\" +\n                \"ReadUpdated=\" + selectedArticle.getReadUpdated() + \"\\n\" +\n                \"StarState=\" + selectedArticle.getStarStatus() + \"\\n\" +\n                \"StarUpdated=\" + selectedArticle.getStarUpdated() + \"\\n\" +\n                \"SaveStatus=\" + selectedArticle.getSaveStatus() + \"\\n\" +\n                \"SaveStatus=\" + selectedArticle.getSaveStatus() + \"\\n\" +\n                \"Pubdate=\" + selectedArticle.getPubDate() + \"\\n\" +\n                \"Crawldate=\" + selectedArticle.getCrawlDate() + \"\\n\" +\n                \"Author=\" + selectedArticle.getAuthor() + \"\\n\" +\n                \"FeedId=\" + selectedArticle.getFeedId() + \"\\n\" +\n                \"Image=\" + selectedArticle.getImage() + \"\\n\" +\n                \"Enclosure=\" + selectedArticle.getEnclosure() + \"\\n\" +\n                \"【Link】\" + selectedArticle.getLink() + \"\\n\" +\n                \"【Summary】\" + selectedArticle.getSummary() + \"\\n\\n\" +\n                \"【Content】\" + document.outerHtml() + \"\\n\";\n\n        new MaterialDialog.Builder(this)\n                .title(R.string.article_info)\n                .content(info)\n                .positiveText(R.string.agree)\n                .onPositive(new MaterialDialog.SingleButtonCallback() {\n                    @Override\n                    public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n                        //获取剪贴板管理器：\n                        ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);\n                        // 创建普通字符型ClipData\n                        ClipData mClipData = ClipData.newPlainText(\"ArticleContent\", ArticleUtil.getPageForDisplay(selectedArticle));\n                        // 将ClipData内容放到系统剪贴板里。\n                        cm.setPrimaryClip(mClipData);\n                        ToastUtils.show(\"已复制文章内容\");\n                    }\n                })\n\n                .positiveColorRes(R.color.material_red_400)\n                .titleGravity(GravityEnum.CENTER)\n                .titleColorRes(R.color.material_red_400)\n                .contentColorRes(android.R.color.white)\n                .backgroundColorRes(R.color.material_blue_grey_800)\n                .dividerColorRes(R.color.material_teal_a400)\n//                .btnSelector(R.drawable.md_btn_selector_custom, DialogAction.POSITIVE)\n                .positiveColor(Color.WHITE)\n                .negativeColorAttr(android.R.attr.textColorSecondaryInverse)\n                .theme(Theme.DARK)\n                .show();\n    }\n\n    /**\n     * 不能使用 onBackPressed，会导致overridePendingTransition转场动画失效\n     * event.getRepeatCount() 后者为短期内重复按下的次数\n     *\n     * @return 返回真表示返回键被屏蔽掉\n     */\n    @Override\n    public boolean onKeyDown(int keyCode, KeyEvent event) {\n        if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {\n            if (video != null && video.isPlaying()) {\n                video.onHideCustomView();\n                return true;\n            }\n            Intent data = new Intent();\n            data.putExtra(\"articleNo\", articleNo);\n            //注意下面的RESULT_OK常量要与回传接收的Activity中onActivityResult()方法一致\n            this.setResult(App.ActivityResult_ArtToMain, data);\n            this.finish();\n            overridePendingTransition(R.anim.fade_in, R.anim.out_from_bottom);\n            return true;\n        }\n        return super.onKeyDown(keyCode, event);\n    }\n\n\n    @Override\n    protected Colorful.Builder buildColorful(Colorful.Builder mColorfulBuilder) {\n        mColorfulBuilder\n                .backgroundColor(R.id.article_root, R.attr.root_view_bg)\n                // 设置 toolbar\n                .backgroundColor(R.id.art_toolbar, R.attr.topbar_bg)\n                //.textColor(R.id.art_toolbar_num, R.attr.topbar_fg)\n                // 设置中屏和底栏之间的分割线\n                .backgroundColor(R.id.article_bottombar_divider, R.attr.bottombar_divider)\n                // 设置 bottombar\n                .backgroundColor(R.id.art_bottombar, R.attr.bottombar_bg)\n                .textColor(R.id.article_bottombar_read, R.attr.bottombar_fg)\n                .textColor(R.id.article_bottombar_star, R.attr.bottombar_fg)\n                .textColor(R.id.article_feed_config, R.attr.topbar_fg)\n                .textColor(R.id.article_bottombar_open_link, R.attr.bottombar_fg)\n                .textColor(R.id.article_bottombar_save, R.attr.bottombar_fg);\n        return mColorfulBuilder;\n    }\n\n\n    private OkHttpClient imgHttpClient;\n\n\n    @Override\n    public void onConfigurationChanged(Configuration config) {\n        super.onConfigurationChanged(config);\n    }\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_article, menu);\n        feedMenuItem = menu.findItem(R.id.article_menu_feed);\n        final Feed feed = CoreDB.i().feedDao().getById(App.i().getUser().getId(), selectedArticle.getFeedId());\n        if (feed != null) {\n            feedMenuItem.setVisible(true);\n        }else {\n            feedMenuItem.setVisible(false);\n        }\n        if(!BuildConfig.DEBUG){\n            MenuItem speak = menu.findItem(R.id.article_menu_speak);\n            speak.setVisible(false);\n            MenuItem articleInfo = menu.findItem(R.id.article_menu_article_info);\n            articleInfo.setVisible(false);\n            MenuItem editContent = menu.findItem(R.id.article_menu_edit_content);\n            editContent.setVisible(false);\n        }\n        return true;\n    }\n\n    MenuItem feedMenuItem;\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        switch (item.getItemId()) {\n            //监听左上角的返回箭头\n            case android.R.id.home:\n                finish();\n                overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);\n                break;\n            case R.id.article_menu_feed:\n                final Feed feed = CoreDB.i().feedDao().getById(App.i().getUser().getId(), selectedArticle.getFeedId());\n                //final View feedConfigView = findViewById(R.id.article_feed_config);\n                if (feed != null) {\n                    Intent intent = new Intent(ArticleActivity.this, FeedActivity.class);\n                    intent.putExtra(\"feedId\", selectedArticle.getFeedId());\n                    startActivity(intent);\n                } else {\n                    ToastUtils.show(\"该订阅源已退订，无法编辑\");\n                }\n                break;\n            case R.id.article_menu_speak:\n                Intent intent = new Intent(ArticleActivity.this, TTSActivity.class);\n                intent.putExtra(\"articleNo\", articleNo);\n                startActivity(intent);\n                break;\n            case R.id.article_menu_article_info:\n                showArticleInfo();\n                break;\n            case R.id.article_menu_edit_content:\n                new MaterialDialog.Builder(ArticleActivity.this)\n                        .title(\"修改文章内容\")\n                        .inputType(InputType.TYPE_CLASS_TEXT)\n                        .inputRange(1, 5600000)\n                        .input(getString(R.string.site_remark), selectedArticle.getContent(), new MaterialDialog.InputCallback() {\n                            @Override\n                            public void onInput(@NonNull MaterialDialog dialog, CharSequence input) {\n                                selectedArticle.setContent(input.toString());\n                                CoreDB.i().articleDao().update(selectedArticle);\n                            }\n                        })\n                        .positiveText(R.string.save)\n                        .negativeText(android.R.string.cancel)\n                        .show();\n                break;\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n\n//    @Override\n//    public boolean onCreateOptionsMenu(Menu menu) {\n//        getMenuInflater().inflate(R.menu.menu_article_activity, menu);\n//        return true;\n//    }\n\n//    private void openMode(){\n    // 调用系统默认的图片查看应用\n//        Intent intentImage = new Intent(Intent.ACTION_VIEW);\n//        intentImage.addCategory(Intent.CATEGORY_DEFAULT);\n//        File file = new File(imageFilePath);\n//        intentImage.setDataAndType(Uri.fromFile(file), \"image/*\");\n//        startActivity(intentImage);\n\n    // 每次都要选择打开方式\n//        startActivity(Intent.createChooser(intentImage, \"请选择一款\"));\n\n    // 调起系统默认的图片查看应用（带有选择为默认）\n//        if(BuildConfig.DEBUG){\n//            Intent openImageIntent = new Intent(Intent.ACTION_VIEW);\n//            openImageIntent.addCategory(Intent.CATEGORY_DEFAULT);\n//            openImageIntent.setDataAndType(Uri.fromFile(new File(imageFilePath)), \"image/*\");\n//            getDefaultActivity(openImageIntent);\n//        }\n//    }\n//    // 获取默认的打开方式\n//    public void getDefaultActivity(Intent intent) {\n//        PackageManager pm = this.getPackageManager();\n//        ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);\n//        // 如果本应用没有询问过是否要选择默认打开方式，并且没有默认的打开方式，打开默认方式选择狂\n//        if (!WithPref.i().hadAskImageOpenMode() || info.activityInfo.packageName.equals(\"android\")) {\n//            WithPref.i().setHadAskImageOpenMode(true);\n//            intent.setComponent(new ComponentName(\"android\", \"com.android.internal.app.ResolverActivity\"));\n//        }\n//        startActivity(intent);\n//        overridePendingTransition(R.anim.fade_in, R.anim.fade_out);\n//        KLog.i(\"打开方式\", \"默认打开方式信息 = \" + info + \";pkgName = \" + info.activityInfo.packageName);\n//    }\n\n    // 打开选择默认打开方式的弹窗\n//    public void startChooseDialog() {\n//        Intent intent = new Intent();\n//        intent.setAction(\"android.intent.action.VIEW\");\n//        intent.addCategory(Intent.CATEGORY_DEFAULT);\n//        intent.setData(Uri.fromFile(new File(imageFilePath)));\n//        intent.setComponent(new ComponentName(\"android\",\"com.android.internal.app.ResolverActivity\"));\n//        startActivity(intent);\n//    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/BaseActivity.java",
    "content": "package me.wizos.loread.activity;\n\nimport android.os.Bundle;\nimport android.view.KeyEvent;\n\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.User;\nimport me.wizos.loread.view.colorful.Colorful;\n\n/**\n * @author Wizos on 2016/3/12.\n */\npublic abstract class BaseActivity extends AppCompatActivity {\n    private static String TAG = \"BaseActivity\";\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        showCurrentTheme();\n    }\n\n//    /**\n//     * // 获取当前的内容供应商\n//     * // 0是未登录\n//     * // 1是本地rss\n//     * // 2是inoreader\n//     * // 3是feedly\n//     * // 4是tinytinyrss\n//     * 初始化Api，Contract，DB\n//     */\n//    public void route() {\n//        Intent intent;\n//        String uid = App.i().getKeyValue().getString(Contract.UID, null);\n//        KLog.e(\"获取UID：\" + uid );\n//        if ( TextUtils.isEmpty(uid) ) {\n//            intent = new Intent(this, ProviderActivity.class);\n//        } else {\n//            intent = new Intent(this, MainActivity.class);\n//        }\n//        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);\n//        startActivity(intent);\n//        overridePendingTransition(R.anim.fade_in, R.anim.fade_out);\n//    }\n\n    protected Colorful mColorful;\n\n    protected abstract Colorful.Builder buildColorful(Colorful.Builder mColorfulBuilder);\n\n    /**\n     * 自动设置当前主题(设置各个视图与颜色属性的关联)\n     */\n    protected void showCurrentTheme() {\n        Colorful.Builder mColorfulBuilder = new Colorful.Builder(this);\n        mColorful = buildColorful(mColorfulBuilder).create();\n        if (App.i().getUser() != null && App.i().getUser().getThemeMode() == App.THEME_NIGHT) {\n            mColorful.setTheme(R.style.AppTheme_Night);\n        } else {\n            mColorful.setTheme(R.style.AppTheme_Day);\n        }\n    }\n\n    /**\n     * 手动切换主题并保存\n     */\n    protected void manualToggleTheme() {\n        User user = App.i().getUser();\n        if (App.i().getUser().getThemeMode() == App.THEME_DAY) {\n            mColorful.setTheme(R.style.AppTheme_Night);\n            user.setThemeMode(App.THEME_NIGHT);\n\n        } else {\n            mColorful.setTheme(R.style.AppTheme_Day);\n            user.setThemeMode(App.THEME_DAY);\n        }\n        CoreDB.i().userDao().update(user);\n    }\n\n\n    @Override\n    public boolean onKeyDown(int keyCode, KeyEvent event) {\n        // 后者为短期内按下的次数\n        if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {\n            this.finish();\n            //返回真表示返回键被屏蔽掉\n            return true;\n        }\n        return super.onKeyDown(keyCode, event);\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/FeedActivity.java",
    "content": "package me.wizos.loread.activity;\n\nimport android.content.ClipData;\nimport android.content.ClipboardManager;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.Color;\nimport android.graphics.drawable.Drawable;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.text.InputType;\nimport android.text.TextUtils;\nimport android.util.ArrayMap;\nimport android.view.LayoutInflater;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.widget.AppCompatButton;\nimport androidx.appcompat.widget.Toolbar;\n\nimport com.afollestad.materialdialogs.DialogAction;\nimport com.afollestad.materialdialogs.MaterialDialog;\nimport com.bumptech.glide.Glide;\nimport com.bumptech.glide.request.RequestOptions;\nimport com.carlt.networklibs.utils.NetworkUtils;\nimport com.google.android.material.floatingactionbutton.FloatingActionButton;\nimport com.hjq.toast.ToastUtils;\nimport com.lxj.xpopup.XPopup;\nimport com.lxj.xpopup.enums.PopupAnimation;\nimport com.noober.background.BackgroundLibrary;\nimport com.noober.background.drawable.DrawableCreator;\nimport com.socks.library.KLog;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.BuildConfig;\nimport me.wizos.loread.R;\nimport me.wizos.loread.bean.feedly.CategoryItem;\nimport me.wizos.loread.bean.feedly.input.EditFeed;\nimport me.wizos.loread.config.SaveDirectory;\nimport me.wizos.loread.config.TestConfig;\nimport me.wizos.loread.config.Unsubscribe;\nimport me.wizos.loread.db.Category;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.Feed;\nimport me.wizos.loread.db.FeedCategory;\nimport me.wizos.loread.network.callback.CallbackX;\nimport me.wizos.loread.utils.ScreenUtil;\nimport me.wizos.loread.utils.UriUtil;\nimport me.wizos.loread.view.IconFontView;\nimport me.wizos.loread.view.colorful.Colorful;\n\npublic class FeedActivity extends BaseActivity {\n    Toolbar toolbar;\n    FloatingActionButton fab;\n    TextView descriptionView;\n    TextView descriptionLabelView;\n    TextView siteLinkLabelView;\n    TextView rssLinkLabelView;\n    TextView subscribersView;\n    TextView updatedView;\n    TextView siteLinkView;\n    TextView feedLinkView;\n    Feed feed;\n    ArrayList<CategoryItem> preCategoryItems;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        BackgroundLibrary.inject2(this);\n        setContentView(R.layout.activity_feed);\n        toolbar = findViewById(R.id.feed_toolbar);\n        setSupportActionBar(toolbar);\n        // 这个小于4.0版本是默认为true，在4.0及其以上是false。该方法的作用：决定左上角的图标是否可以点击(没有向左的小图标)，true 可点\n        getSupportActionBar().setHomeButtonEnabled(true);\n        // 决定左上角图标的左侧是否有向左的小箭头，true 有小箭头\n        getSupportActionBar().setDisplayHomeAsUpEnabled(true);\n        getSupportActionBar().setDisplayShowTitleEnabled(true);\n\n        fab = findViewById(R.id.fab);\n        descriptionLabelView = findViewById(R.id.feed_desc_label);\n        siteLinkLabelView = findViewById(R.id.feed_site_link_label);\n        rssLinkLabelView = findViewById(R.id.feed_rss_link_label);\n        descriptionView = findViewById(R.id.feed_description);\n        subscribersView = findViewById(R.id.feed_subscribers);\n        updatedView = findViewById(R.id.feed_updated);\n        siteLinkView = findViewById(R.id.feed_site_link);\n        feedLinkView = findViewById(R.id.feed_rss_link);\n\n        Bundle bundle;\n        if (savedInstanceState != null) {\n            bundle = savedInstanceState;\n        } else {\n            bundle = getIntent().getExtras();\n        }\n\n        if (bundle == null) {\n            return;\n        }\n\n        String feedId = bundle.getString(\"feedId\");\n        if (TextUtils.isEmpty(feedId)) {\n            return;\n        }\n        feed = CoreDB.i().feedDao().getById(App.i().getUser().getId(),feedId);\n        if( null == feed){\n            finish();\n            return;\n        }\n        String feedUrlId = \"feed/\" + feed.getFeedUrl();\n        KLog.i(\"展示feed的详情：\" + feedId + \",\"  + feedUrlId + \" , \" + feed);\n\n        RequestOptions options = new RequestOptions().circleCrop();\n\n        Glide.with(this).load(UriUtil.getFaviconUrl(feed.getHtmlUrl())).apply(options).into(fab);\n\n        getSupportActionBar().setTitle(feed.getTitle());\n        toolbar.setSubtitle(feed.getFeedUrl());\n        siteLinkView.setText(feed.getHtmlUrl());\n        feedLinkView.setText(feed.getFeedUrl());\n        fab.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View view) {\n                if(TextUtils.isEmpty(feed.getHtmlUrl())){\n                    //ToastUtils.show();\n                    return;\n                }\n                Intent intent = new Intent(FeedActivity.this, WebActivity.class);\n                intent.setData(Uri.parse(feed.getHtmlUrl()));\n                startActivity(intent);\n                overridePendingTransition(R.anim.fade_in, R.anim.fade_out);\n            }\n        });\n\n//        Retrofit retrofit = new Retrofit.Builder()\n//                .baseUrl(FeedlyApi.HOST + \"/\") // 设置网络请求的Url地址, 必须以/结尾\n//                .client(HttpClientManager.i().simpleClient())\n//                .addConverterFactory(GsonConverterFactory.create())  // 设置数据解析器\n//                .build();\n//\n//        FeedlyService feedlyService = retrofit.create(FeedlyService.class);\n//\n//        //对 发送请求 进行封装\n//        Call<FeedItem> callFeedMeta = feedlyService.getFeedMeta(feedUrlId);\n//\n//        callFeedMeta.enqueue(new Callback<FeedItem>() {\n//            //请求成功时回调\n//            @Override\n//            public void onResponse(Call<FeedItem> call, Response<FeedItem> response) {\n//                if (!response.isSuccessful()) {\n//                    return;\n//                }\n//\n//                FeedItem feedItem = response.body();\n//                // 对返回数据进行处理\n//                //KLog.e(\"取到数据：\" + response.body().toString());\n//                if (!TextUtils.isEmpty(feedItem.getDescription())) {\n////                    descriptionLabelView.setVisibility(View.VISIBLE);\n//                    descriptionView.setVisibility(View.VISIBLE);\n//                    descriptionView.setText(feedItem.getDescription());\n//                }\n//\n//                subscribersView.setVisibility(View.VISIBLE);\n//                updatedView.setVisibility(View.VISIBLE);\n//\n//                subscribersView.setText(feedItem.getSubscribers() + \" 关注\");\n//                updatedView.setText(TimeUtil.stampToTime(feedItem.getUpdated(), \"yyyy-MM-dd\") + \" 更新\");\n//            }\n//\n//            //请求失败时候的回调\n//            @Override\n//            public void onFailure(Call<FeedItem> call, Throwable throwable) {\n//                KLog.e(\"连接失败\" + throwable);\n//            }\n//        });\n\n        createItemView2(feed);\n    }\n\n    private View categoryView;\n    private View remarkView;\n\n    private void createItemView2(final Feed feed) {\n        LayoutInflater inflater = getLayoutInflater();\n        LinearLayout linearLayout = findViewById(R.id.feed_summary);\n\n        // 增加设置项\n        View settingSession = inflater.inflate(R.layout.setting_item_session, linearLayout, false);\n        ((TextView) settingSession.findViewById(R.id.setting_session_title)).setText(R.string.settings);\n        linearLayout.addView(settingSession);\n\n        remarkView = inflater.inflate(R.layout.setting_item_arrow, linearLayout, false);\n        ((TextView) remarkView.findViewById(R.id.setting_item_title)).setText(R.string.remark);\n        if (!TextUtils.isEmpty(feed.getTitle())) {\n            ((TextView) remarkView.findViewById(R.id.setting_item_value)).setText(feed.getTitle());\n        } else {\n            ((TextView) remarkView.findViewById(R.id.setting_item_value)).setText(R.string.unknown);\n        }\n        remarkView.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                new MaterialDialog.Builder(FeedActivity.this)\n                        .title(R.string.site_remark)\n                        .inputType(InputType.TYPE_CLASS_TEXT)\n                        .inputRange(1, 56)\n                        .input(getString(R.string.site_remark), feed.getTitle(), new MaterialDialog.InputCallback() {\n                            @Override\n                            public void onInput(@NonNull MaterialDialog dialog, CharSequence input) {\n                                if (!NetworkUtils.isAvailable()) {\n                                    ToastUtils.show(getString(R.string.tips_no_net));\n                                } else {\n                                    renameFeed(input.toString(), feed);\n                                    dialog.dismiss();\n                                }\n                            }\n                        })\n                        .positiveText(R.string.confirm)\n                        .negativeText(android.R.string.cancel)\n                        .show();\n            }\n        });\n        linearLayout.addView(remarkView);\n\n        final EditFeed editFeed = new EditFeed(feed.getId());\n        preCategoryItems = editFeed.getCategoryItems();\n        final String[] preCategoryTitles = new String[preCategoryItems.size()];\n        for (int i = 0, size = preCategoryItems.size(); i < size; i++) {\n            preCategoryTitles[i] = preCategoryItems.get(i).getLabel();\n        }\n        String titles = TextUtils.join(\" / \", preCategoryTitles);\n\n\n        categoryView = inflater.inflate(R.layout.setting_item_arrow, linearLayout, false);\n        ((TextView) categoryView.findViewById(R.id.setting_item_title)).setText(getString(R.string.category));\n        if (!TextUtils.isEmpty(titles)) {\n            ((TextView) categoryView.findViewById(R.id.setting_item_value)).setText(titles);\n        } else {\n            ((TextView) categoryView.findViewById(R.id.setting_item_value)).setText(getString(R.string.no_thing));\n        }\n        categoryView.setOnClickListener(v -> {\n            final List<Category> categoryList = CoreDB.i().categoryDao().getAll(App.i().getUser().getId());\n            ArrayMap<String, Integer> categoryMap = new ArrayMap<>(categoryList.size());\n\n            String[] categoryTitleArray = new String[categoryList.size()];\n            for (int i = 0, size = categoryList.size(); i < size; i++) {\n                categoryMap.put(categoryList.get(i).getId(), i);\n                categoryTitleArray[i] = categoryList.get(i).getTitle();\n            }\n\n            final Integer[] beforeSelectedIndices = new Integer[]{preCategoryItems.size()};\n            for (int i = 0, size = preCategoryItems.size(); i < size; i++) {\n                beforeSelectedIndices[i] = categoryMap.get(preCategoryItems.get(i).getId());\n            }\n\n            new MaterialDialog.Builder(FeedActivity.this)\n                    .title(getString(R.string.edit_category))\n                    .items(categoryTitleArray)\n                    .itemsCallbackMultiChoice(beforeSelectedIndices, new MaterialDialog.ListCallbackMultiChoice() {\n                        @Override\n                        public boolean onSelection(MaterialDialog dialog, final Integer[] which, CharSequence[] text) {\n                            final ArrayList<CategoryItem> selectedCategoryItems = new ArrayList<>();\n                            ArrayList<String> selectedTitles = new ArrayList<>();\n                            CategoryItem categoryItem;\n                            for (int i : which) {\n                                categoryItem = new CategoryItem();\n                                categoryItem.setId(categoryList.get(i).getId());\n                                categoryItem.setLabel(categoryList.get(i).getTitle());\n                                selectedCategoryItems.add(categoryItem);\n                                selectedTitles.add(categoryList.get(i).getTitle());\n                            }\n                            final String titles1 = TextUtils.join(\" / \", selectedTitles);\n                            editFeed.setCategoryItems(selectedCategoryItems);\n                            ToastUtils.show(R.string.editing);\n                            App.i().getApi().editFeedCategories(preCategoryItems, editFeed, new CallbackX<String,String>() {\n                                @Override\n                                public void onSuccess(String result) {\n                                    ArrayList<FeedCategory> feedCategories = new ArrayList<>(selectedCategoryItems.size());\n                                    FeedCategory feedCategory;\n                                    for (CategoryItem categoryItem : selectedCategoryItems) {\n                                        feedCategory = new FeedCategory(App.i().getUser().getId(), feed.getId(), categoryItem.getId());\n                                        feedCategories.add(feedCategory);\n                                    }\n                                    ((TextView) categoryView.findViewById(R.id.setting_item_value)).setText(titles1);\n                                    CoreDB.i().coverFeedCategories(editFeed);\n                                    preCategoryItems = selectedCategoryItems;\n                                    ToastUtils.show(R.string.edit_success);\n                                }\n\n                                @Override\n                                public void onFailure(String error) {\n                                    ToastUtils.show(R.string.edit_fail);\n                                }\n                            });\n                            return true;\n                        }\n                    })\n                    .alwaysCallMultiChoiceCallback() // the callback will always be called, to check if selection is still allowed\n                    .show();\n\n        });\n        linearLayout.addView(categoryView);\n\n        final View displayView = inflater.inflate(R.layout.setting_item_arrow, linearLayout, false);\n        ((TextView) displayView.findViewById(R.id.setting_item_title)).setText(R.string.select_display_mode);\n        final TextView displayValueView = displayView.findViewById(R.id.setting_item_value);\n        displayValueView.setText(TestConfig.i().getDisplayMode(feed.getId()));\n        displayView.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                new XPopup.Builder(FeedActivity.this)\n                        .isCenterHorizontal(true) //是否与目标水平居中对齐\n                        .offsetY(-10)\n                        .hasShadowBg(true)\n                        .popupAnimation(PopupAnimation.ScaleAlphaFromCenter)\n                        .atView(displayValueView)  // 依附于所点击的View，内部会自动判断在上方或者下方显示\n                        .asAttachList(new String[]{getString(R.string.rss), getString(R.string.readability), getString(R.string.original)},\n                                null,\n                                (which, text) -> {\n                                    String selectedFeedDisplayMode = App.DISPLAY_RSS;\n\n                                    switch (which) {\n                                        case 0:\n                                            selectedFeedDisplayMode = App.DISPLAY_RSS;\n                                            break;\n                                        case 1:\n                                            selectedFeedDisplayMode = App.DISPLAY_READABILITY;\n                                            break;\n                                        case 2:\n                                            selectedFeedDisplayMode = App.DISPLAY_LINK;\n                                            break;\n                                        default:\n                                            break;\n                                    }\n\n                                    if (!TextUtils.isEmpty(TestConfig.i().getDisplayMode(feed.getId()))) {\n                                        if (selectedFeedDisplayMode.equals(TestConfig.i().getDisplayMode(feed.getId()))) {\n                                            return;\n                                        }\n                                        TestConfig.i().removeDisplayRouter(feed.getId());\n                                        TestConfig.i().addDisplayRouter(feed.getId(), selectedFeedDisplayMode);\n                                        TestConfig.i().save();\n                                        displayValueView.setText(selectedFeedDisplayMode);\n                                    }\n                                })\n                        .show();\n            }\n        });\n        linearLayout.addView(displayView);\n\n\n\n\n        if(!BuildConfig.DEBUG){\n            return;\n        }\n        View saveFolderView = inflater.inflate(R.layout.setting_item_arrow, linearLayout, false);\n        ((TextView) saveFolderView.findViewById(R.id.setting_item_title)).setText(R.string.save_directory);\n\n        final String optionName = SaveDirectory.i().getDirNameSettingByFeed(feed.getId());\n        final TextView saveFolderValueView = saveFolderView.findViewById(R.id.setting_item_value);\n        saveFolderValueView.setText(optionName);\n\n        saveFolderView.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n\n                new XPopup.Builder(FeedActivity.this)\n                        .isCenterHorizontal(true) //是否与目标水平居中对齐\n                        .offsetY(-10)\n                        .hasShadowBg(true)\n                        .popupAnimation(PopupAnimation.ScaleAlphaFromCenter)\n                        .atView(saveFolderValueView)  // 依附于所点击的View，内部会自动判断在上方或者下方显示\n                        .asAttachList(SaveDirectory.i().getDirectoriesOptionName(),\n                                null,\n                                (which, text) -> {\n                                    List<String> dirsValue = SaveDirectory.i().getDirectoriesOptionValue();\n                                    SaveDirectory.i().setFeedDirectory(feed.getId(),dirsValue.get(which));\n                                    SaveDirectory.i().save();\n                                })\n                        .show();\n            }\n        });\n        linearLayout.addView(saveFolderView);\n    }\n\n    public void renameFeed(final String renamedTitle, final Feed feed) {\n        KLog.e(\"=====\" + renamedTitle + feed.getId());\n        if (renamedTitle.equals(\"\") || feed.getTitle().equals(renamedTitle)) {\n            return;\n        }\n        App.i().getApi().renameFeed(feed.getId(), renamedTitle, new CallbackX() {\n            @Override\n            public void onSuccess(Object result) {\n                feed.setTitle(renamedTitle);\n                CoreDB.i().feedDao().update(feed);\n                ToastUtils.show(R.string.edit_success);\n                KLog.e(\"改了名字：\" + renamedTitle);\n            }\n\n            @Override\n            public void onFailure(Object error) {\n                ToastUtils.show(App.i().getString(R.string.rename_failed));\n            }\n        });\n    }\n\n    public void copyIconUrl(@Nullable View view) {\n        if (feed == null || TextUtils.isEmpty(feed.getIconUrl())) {\n            return;\n        }\n        //获取剪贴板管理器：\n        ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);\n        // 创建普通字符型ClipData\n        ClipData mClipData = ClipData.newRawUri(feed.getTitle(), Uri.parse(feed.getIconUrl()));\n        // 将ClipData内容放到系统剪贴板里。\n        cm.setPrimaryClip(mClipData);\n        ToastUtils.show(R.string.copy_success);\n    }\n\n    public void copyHtmlUrl(@Nullable View view) {\n        if (feed == null || TextUtils.isEmpty(feed.getHtmlUrl())) {\n            return;\n        }\n        //获取剪贴板管理器：\n        ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);\n        // 创建普通字符型ClipData\n        ClipData mClipData = ClipData.newRawUri(feed.getTitle(), Uri.parse(feed.getHtmlUrl()));\n        // 将ClipData内容放到系统剪贴板里。\n        cm.setPrimaryClip(mClipData);\n        ToastUtils.show(R.string.copy_success);\n    }\n\n    public void copyFeedUrl(@Nullable View view) {\n        if (feed==null || TextUtils.isEmpty(feed.getFeedUrl())) {\n            return;\n        }\n        //获取剪贴板管理器：\n        ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);\n        // 创建普通字符型ClipData\n        ClipData mClipData = ClipData.newRawUri(feed.getTitle(), Uri.parse(feed.getFeedUrl()));\n        // 将ClipData内容放到系统剪贴板里。\n        cm.setPrimaryClip(mClipData);\n        ToastUtils.show(R.string.copy_success);\n    }\n\n    private Integer[] selectIndices;\n\n    public void showSelectFolder(final View view, final String feedId) {\n        final List<Category> categoryList = CoreDB.i().categoryDao().getAll(App.i().getUser().getId());\n        String[] categoryTitleArray = new String[categoryList.size()];\n        for (int i = 0, size = categoryList.size(); i < size; i++) {\n            categoryTitleArray[i] = categoryList.get(i).getTitle();\n        }\n        final EditFeed editFeed = new EditFeed();\n        editFeed.setId(feedId);\n        new MaterialDialog.Builder(this)\n                .title(getString(R.string.select_category))\n                .items(categoryTitleArray)\n                .alwaysCallMultiChoiceCallback()\n                .itemsCallbackMultiChoice(null, (dialog, which, text) -> {\n                    FeedActivity.this.selectIndices = which;\n                    for (int i : which) {\n                        KLog.e(\"点选了：\" + i);\n                    }\n                    return true;\n                })\n                .positiveText(R.string.confirm)\n                .onPositive((dialog, which) -> {\n                    ArrayList<CategoryItem> categoryItemList = new ArrayList<>();\n                    for (Integer selectIndex : selectIndices) {\n                        CategoryItem categoryItem = new CategoryItem();\n                        categoryItem.setId(categoryList.get(selectIndex).getId());\n                        categoryItemList.add(categoryItem);\n                    }\n                    editFeed.setCategoryItems(categoryItemList);\n                    view.setClickable(false);\n                    App.i().getApi().addFeed(editFeed, new CallbackX() {\n                        @Override\n                        public void onSuccess(Object result) {\n                            KLog.e(\"添加成功\");\n                            ((IconFontView) view).setText(R.string.font_tick);\n                            ToastUtils.show(R.string.subscribe_success);\n                            view.setClickable(true);\n                        }\n\n                        @Override\n                        public void onFailure(Object error) {\n                            ToastUtils.show(getString(R.string.subscribe_fail));\n                            view.setClickable(true);\n                        }\n                    });\n                }).show();\n    }\n\n    public void clickUnsubscribe(final View view) {\n        if (feed == null) {\n            return;\n        }\n        if (CoreDB.i().feedDao().getById(App.i().getUser().getId(), feed.getId()) == null) {\n            showSelectFolder(view, feed.getId());\n        } else {\n            new MaterialDialog.Builder(this)\n                    .title(R.string.warning)\n                    .content(R.string.are_you_sure_that_unsubscribe_this_feed_link)\n                    .positiveText(R.string.confirm)\n                    .negativeText(R.string.cancel)\n                    .positiveColor(Color.RED)\n                    .onPositive(new MaterialDialog.SingleButtonCallback() {\n                        @Override\n                        public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n                            App.i().getApi().unsubscribeFeed(feed.getId(), new CallbackX() {\n                                @Override\n                                public void onSuccess(Object result) {\n                                    KLog.e(\"退订成功\");\n                                    ToastUtils.show(getString(R.string.unsubscribe_succeeded));\n                                    ((AppCompatButton) view).setText(R.string.subscribe);\n                                    Drawable drawable = new DrawableCreator.Builder()\n                                            .setRipple(true, getResources().getColor(R.color.primary))\n                                            .setPressedSolidColor(getResources().getColor(R.color.primary), getResources().getColor(R.color.bluePrimary))\n                                            .setSolidColor(getResources().getColor(R.color.bluePrimary))\n                                            .setCornersRadius(ScreenUtil.dp2px(30))\n                                            .build();\n                                    view.setBackground(drawable);\n\n                                    List<Feed> feeds = new ArrayList<>();\n                                    feeds.add(feed);\n                                    Unsubscribe.genBackupFile2(App.i().getUser(), feeds);\n                                    CoreDB.i().feedCategoryDao().deleteByFeedId(feed.getUid(), feed.getId());\n                                    CoreDB.i().articleDao().deleteUnStarByFeedId(feed.getUid(), feed.getId());\n                                    CoreDB.i().feedDao().delete(feed);\n                                }\n\n                                @Override\n                                public void onFailure(Object error) {\n                                    KLog.e(\"失败：\" + error);\n                                    ToastUtils.show(getString(R.string.unsubscribe_failed,error));\n                                }\n                            });\n\n                        }\n                    }).build().show();\n        }\n    }\n\n\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        //监听左上角的返回箭头\n        if (item.getItemId() == android.R.id.home) {\n            finish();\n            overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);\n        }\n        return super.onOptionsItemSelected(item);\n    }\n    @Override\n    protected Colorful.Builder buildColorful(Colorful.Builder mColorfulBuilder) {\n        return mColorfulBuilder;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/LabActivity.java",
    "content": "package me.wizos.loread.activity;\n\nimport android.content.Intent;\nimport android.content.pm.PackageManager;\nimport android.content.pm.ResolveInfo;\nimport android.net.Uri;\nimport android.os.AsyncTask;\nimport android.os.Bundle;\nimport android.util.ArrayMap;\nimport android.view.View;\nimport android.widget.EditText;\n\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.work.Constraints;\nimport androidx.work.NetworkType;\nimport androidx.work.PeriodicWorkRequest;\nimport androidx.work.WorkManager;\n\nimport com.afollestad.materialdialogs.MaterialDialog;\nimport com.hjq.toast.ToastUtils;\nimport com.socks.library.KLog;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.concurrent.TimeUnit;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.config.AdBlock;\nimport me.wizos.loread.config.ArticleActionConfig;\nimport me.wizos.loread.config.LinkRewriteConfig;\nimport me.wizos.loread.config.NetworkRefererConfig;\nimport me.wizos.loread.config.NetworkUserAgentConfig;\nimport me.wizos.loread.config.TestConfig;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.db.ArticleTag;\nimport me.wizos.loread.db.Category;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.Tag;\nimport me.wizos.loread.db.User;\nimport me.wizos.loread.network.SyncWorker;\nimport me.wizos.loread.utils.EncryptUtil;\nimport me.wizos.loread.utils.FileUtil;\nimport me.wizos.loread.utils.StringUtils;\n\nimport static androidx.work.ExistingPeriodicWorkPolicy.KEEP;\nimport static me.wizos.loread.Contract.SCHEMA_HTTP;\nimport static me.wizos.loread.Contract.SCHEMA_HTTPS;\n\n\npublic class LabActivity extends AppCompatActivity {\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_lab);\n    }\n\n\n    private MaterialDialog materialDialog;\n    public void onClickBackup(View view) {\n        materialDialog = new MaterialDialog.Builder(this)\n                .title(\"正在处理\")\n                .content(\"请耐心等待下\")\n                .progress(true, 0)\n                .canceledOnTouchOutside(false)\n                .progressIndeterminateStyle(false)\n                .show();\n        new Thread(new Runnable() {\n            @Override\n            public void run() {\n                FileUtil.backup();\n                materialDialog.dismiss();\n            }\n        }).start();\n\n    }\n\n    public void onClickRestore(View view) {\n        materialDialog = new MaterialDialog.Builder(this)\n                .title(\"正在处理\")\n                .content(\"请耐心等待下\")\n                .progress(true, 0)\n                .canceledOnTouchOutside(false)\n                .progressIndeterminateStyle(false)\n                .show();\n        new Thread(new Runnable() {\n            @Override\n            public void run() {\n                FileUtil.restore();\n                materialDialog.dismiss();\n            }\n        }).start();\n    }\n\n\n    public void onClickReadConfig(View view) {\n        materialDialog = new MaterialDialog.Builder(this)\n                .content(\"正在读取\")\n                .progress(true, 0)\n                .canceledOnTouchOutside(false)\n                .progressIndeterminateStyle(false)\n                .show();\n        TestConfig.i().reset();\n        AdBlock.i().reset();\n        LinkRewriteConfig.i().reset();\n        NetworkRefererConfig.i().reset();\n        NetworkUserAgentConfig.i().reset();\n        // UserConfig.i().reset();\n        materialDialog.dismiss();\n    }\n\n\n    public void onClickArrangeCrawlDateArticle(View view) {\n        AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {\n            @Override\n            public void run() {\n                List<Article> articles = CoreDB.i().articleDao().getAllNoOrder(App.i().getUser().getId());\n                for (Article article:articles) {\n                    article.setCrawlDate(article.getPubDate());\n                }\n                CoreDB.i().articleDao().update(articles);\n                KLog.i(\"整理完成：\" + articles.size());\n            }\n        });\n    }\n\n    public void onClickClearHtmlDir(View view) {\n        AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {\n            @Override\n            public void run() {\n                clearHtmlDir();\n            }\n        });\n    }\n\n    /**\n     * 将某些没有被清理到的缓存文件夹给清理掉\n     */\n    private void clearHtmlDir() {\n        //List<Article> articles = WithDB.i().getArtsAllNoOrder();\n        List<Article> articles = CoreDB.i().articleDao().getAllNoOrder(App.i().getUser().getId());\n        ArrayMap<String, String> temp = new ArrayMap<>(articles.size());\n\n        for (Article article : articles) {\n            temp.put(EncryptUtil.MD5(article.getId()), \"1\");\n        }\n\n\n        File dir = new File(App.i().getUserCachePath());\n        File[] arts = dir.listFiles();\n        KLog.e(\"文件数量：\" + arts.length);\n        String x = \"\";\n        for (File sourceFile : arts) {\n            x = temp.get(sourceFile.getName());\n            if (null == x) {\n                KLog.e(\"移动文件名：\" + \"   \" + sourceFile.getName());\n                FileUtil.moveDir(sourceFile.getAbsolutePath(), App.i().getUserFilesDir() + \"/move/\" + sourceFile.getName());\n            }\n        }\n    }\n\n\n    public void startSyncWorkManager(View view) {\n        ToastUtils.show(\"开始 同步WorkManager\");\n        // Constraints 指明工作何时可以运行\n        Constraints constraints = new Constraints.Builder()\n                .setRequiredNetworkType(NetworkType.CONNECTED)\n                .build();\n        PeriodicWorkRequest syncRequest = new PeriodicWorkRequest.Builder(SyncWorker.class, 15, TimeUnit.MINUTES)\n                .setConstraints(constraints)\n                .build();\n        WorkManager.getInstance(this).enqueueUniquePeriodicWork(SyncWorker.TAG,KEEP,syncRequest);\n    }\n\n    public void stopWorkManager(View view) {\n        ToastUtils.show(\"取消 WorkManager\");\n        WorkManager.getInstance(this).cancelAllWork();\n    }\n\n\n    public void openActivity(View view){\n        EditText editText = findViewById(R.id.lab_enter_edittext);\n        String url = editText.getText().toString();\n        KLog.e( \"获取的url：\" + url );\n        int enterSize = getMatchActivitiesSize(url);\n        int wizosSize = getMatchActivitiesSize(\"https://wizos.me\");\n        Intent intent;\n        if( !App.i().getUser().isOpenLinkBySysBrowser() && (url.startsWith(SCHEMA_HTTP) || url.startsWith(SCHEMA_HTTPS)) && enterSize == wizosSize){\n            intent = new Intent(LabActivity.this, WebActivity.class);\n            intent.setData(Uri.parse(url));\n            intent.putExtra(\"theme\", App.i().getUser().getThemeMode());\n        }else {\n            intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));\n        }\n        startActivity(intent);\n        overridePendingTransition(R.anim.fade_in, R.anim.fade_out);\n    }\n\n    private int getMatchActivitiesSize(String url){\n        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));\n        PackageManager packageManager = getPackageManager();\n        List<ResolveInfo> list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);\n        for (ResolveInfo resolveInfo:list) {\n            KLog.e( \"适配的包名：\" + resolveInfo.activityInfo.packageName );\n        }\n        return list.size();\n    }\n\n//    public void loginAccount(){\n//        // TODO: 2020/4/14 开始模拟登录\n//        if(!Config.i().enableAuth){\n//            return;\n//        }\n//        handleAccount();\n//\n//        Account account = new Account(getString(R.string.app_name),ACCOUNT_TYPE);\n//        // 帐户密码和信息这里用null演示\n//        mAccountManager.addAccountExplicitly(account, null, null);\n//        // 自动同步\n//        Bundle bundle= new Bundle();\n//        ContentResolver.setIsSyncable(account, AccountProvider.AUTHORITY, 1);\n//        ContentResolver.setSyncAutomatically(account, AccountProvider.AUTHORITY,true);\n//        ContentResolver.addPeriodicSync(account, AccountProvider.AUTHORITY,bundle, 30);    // 间隔时间为30秒\n//        // 手动同步\n////        ContentResolver.requestSync(account, AccountProvider.AUTHORITY, bundle);\n////        finish();\n//    }\n\n    public void onClickClearTags(View view) {\n        CoreDB.i().tagDao().clear(App.i().getUser().getId());\n        CoreDB.i().articleTagDao().clear(App.i().getUser().getId());\n    }\n\n    public void onClickGenTags(View view) {\n        String uid = App.i().getUser().getId();\n        List<Article> articles = CoreDB.i().articleDao().getNotTagStar(uid,0);\n        List<ArticleTag> articleTags = new ArrayList<>();\n        KLog.e(\"设置 没有tag的 数据：\" + articles.size() );\n        Set<String> tagTitleSet = new HashSet<>();\n        for (Article article: articles){\n            KLog.e(\"article feedId：\" + article.getFeedId() );\n            if(StringUtils.isEmpty(article.getFeedId())){\n                continue;\n            }\n            KLog.e(\"article 数据：\" + article);\n            List<Category> categories = CoreDB.i().categoryDao().getByFeedId(uid,article.getFeedId());\n            for (Category category:categories) {\n                tagTitleSet.add(category.getTitle());\n                ArticleTag articleTag = new ArticleTag(uid, article.getId(), category.getId());\n                articleTags.add(articleTag );\n                KLog.e(\"设置 articleTag 数据：\" + articleTag);\n            }\n        }\n\n        List<Tag> tags = new ArrayList<>(tagTitleSet.size());\n        for (String title:tagTitleSet) {\n            Tag tag = new Tag();\n            tag.setUid(uid);\n            tag.setId(title);\n            tag.setTitle(title);\n            tags.add(tag);\n            KLog.e(\"设置 Tag 数据：\" + tag);\n        }\n        CoreDB.i().tagDao().insert(tags);\n        CoreDB.i().articleTagDao().insert(articleTags);\n    }\n\n    public void onClickEditHost(View view) {\n        User user = App.i().getUser();\n        if(user==null){\n            ToastUtils.show(\"当前用户不存在\");\n            return;\n        }\n\n        EditText editText = findViewById(R.id.lab_enter_edittext);\n        String url = editText.getText().toString();\n        user.setHost(url);\n        CoreDB.i().userDao().insert(user);\n    }\n\n    public void actionArticle(View view){\n        User user = App.i().getUser();\n        if(user==null){\n            ToastUtils.show(\"当前用户不存在\");\n            return;\n        }\n        ArticleActionConfig.i().exeRules(App.i().getUser().getId(), 0);\n    }\n\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/MainActivity.java",
    "content": "package me.wizos.loread.activity;\n\nimport android.annotation.SuppressLint;\nimport android.app.AlertDialog;\nimport android.content.DialogInterface;\nimport android.content.Intent;\nimport android.graphics.Color;\nimport android.os.AsyncTask;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.text.InputType;\nimport android.view.KeyEvent;\nimport android.view.View;\nimport android.view.ViewConfiguration;\nimport android.view.ViewGroup;\nimport android.widget.CompoundButton;\nimport android.widget.ImageView;\nimport android.widget.RadioButton;\nimport android.widget.RadioGroup;\nimport android.widget.RelativeLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.widget.Toolbar;\nimport androidx.lifecycle.Observer;\nimport androidx.lifecycle.ViewModelProvider;\nimport androidx.paging.PagedList;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.LinearSmoothScroller;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.SimpleItemAnimator;\nimport androidx.work.Constraints;\nimport androidx.work.ExistingPeriodicWorkPolicy;\nimport androidx.work.NetworkType;\nimport androidx.work.OneTimeWorkRequest;\nimport androidx.work.PeriodicWorkRequest;\nimport androidx.work.WorkManager;\n\nimport com.afollestad.materialdialogs.MaterialDialog;\nimport com.afollestad.materialdialogs.simplelist.MaterialSimpleListAdapter;\nimport com.afollestad.materialdialogs.simplelist.MaterialSimpleListItem;\nimport com.google.android.material.bottomsheet.BottomSheetDialog;\nimport com.hjq.permissions.OnPermission;\nimport com.hjq.permissions.Permission;\nimport com.hjq.permissions.XXPermissions;\nimport com.hjq.toast.ToastUtils;\nimport com.jeremyliao.liveeventbus.LiveEventBus;\nimport com.kyleduo.switchbutton.SwitchButton;\nimport com.lxj.xpopup.XPopup;\nimport com.lxj.xpopup.enums.PopupAnimation;\nimport com.lxj.xpopup.interfaces.OnSelectListener;\nimport com.socks.library.KLog;\nimport com.yanzhenjie.recyclerview.OnItemClickListener;\nimport com.yanzhenjie.recyclerview.OnItemLongClickListener;\nimport com.yanzhenjie.recyclerview.OnItemMenuClickListener;\nimport com.yanzhenjie.recyclerview.OnItemSwipeListener;\nimport com.yanzhenjie.recyclerview.SwipeMenu;\nimport com.yanzhenjie.recyclerview.SwipeMenuBridge;\nimport com.yanzhenjie.recyclerview.SwipeMenuCreator;\nimport com.yanzhenjie.recyclerview.SwipeMenuItem;\nimport com.yanzhenjie.recyclerview.SwipeRecyclerView;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.concurrent.TimeUnit;\n\nimport butterknife.ButterKnife;\nimport butterknife.OnClick;\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.adapter.ArticlePagedListAdapter;\nimport me.wizos.loread.adapter.ExpandedAdapter;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.db.Collection;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.User;\nimport me.wizos.loread.network.SyncWorker;\nimport me.wizos.loread.network.callback.CallbackX;\nimport me.wizos.loread.utils.SnackbarUtil;\nimport me.wizos.loread.utils.TimeUtil;\nimport me.wizos.loread.view.IconFontView;\nimport me.wizos.loread.view.SwipeRefreshLayoutS;\nimport me.wizos.loread.view.colorful.Colorful;\nimport me.wizos.loread.view.colorful.setter.ViewGroupSetter;\nimport me.wizos.loread.viewmodel.ArticleViewModel;\n\n\n/**\n * @author Wizos on 2016‎年5‎月23‎日\n */\npublic class MainActivity extends BaseActivity implements SwipeRefreshLayoutS.OnRefreshListener {\n    private static final String TAG = \"MainActivity\";\n    private IconFontView vPlaceHolder;\n    private ImageView vToolbarAutoMark;\n    private Toolbar toolbar;\n    private SwipeRefreshLayoutS swipeRefreshLayoutS;\n    private SwipeRecyclerView articleListView;\n    // private MultiTypeAdapter articlesAdapter;\n    private ArticlePagedListAdapter articlesAdapter;\n    private IconFontView refreshIcon;\n\n    // 方案3\n    private SwipeRecyclerView tagListView;\n    private ExpandedAdapter tagListAdapter;\n\n    private TextView countTips;\n\n    private Integer[] scrollIndex;\n    private View articlesHeaderView;\n\n    private BottomSheetDialog quickSettingDialog;\n    private BottomSheetDialog tagBottomSheetDialog;\n    private RelativeLayout relativeLayout;\n    //private StickyHeaderLayout stickyHeaderLayout;\n    private boolean autoMarkReaded = false;\n    private static Handler maHandler = new Handler();\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        setContentView(R.layout.activity_main);\n        ButterKnife.bind(this);\n        initToolbar();\n        initIconView();\n        initArtListView();\n        initTagListView();\n        initSwipeRefreshLayout(); // 必须要放在 initArtListView() 之后，不然无论 ListView 滚动到第几页，一下拉就会触发刷新\n        showAutoSwitchThemeSnackBar();\n        applyPermissions();\n        super.onCreate(savedInstanceState);// 由于使用了自动换主题，所以要放在这里\n        getArtData();  // 获取文章列表数据为 App.articleList\n        autoMarkReaded = App.i().getUser().isMarkReadOnScroll();\n        initWorkRequest();\n    }\n\n    private void initWorkRequest(){\n//        Constraints.Builder builder = new Constraints.Builder();\n//        if(App.i().getUser().isAutoSync()){\n//            if( App.i().getUser().isAutoSyncOnlyWifi() ){\n//                builder.setRequiredNetworkType(NetworkType.UNMETERED);\n//            }else {\n//                builder.setRequiredNetworkType(NetworkType.CONNECTED);\n//            }\n//            PeriodicWorkRequest syncRequest = new PeriodicWorkRequest.Builder(SyncWorker.class, App.i().getUser().getAutoSyncFrequency(), TimeUnit.MINUTES)\n//                    .setConstraints(builder.build())\n//                    .addTag(SyncWorker.TAG)\n//                    .build();\n//            WorkManager.getInstance(this).enqueueUniquePeriodicWork(SyncWorker.TAG, ExistingPeriodicWorkPolicy.KEEP,syncRequest);\n//            KLog.i(\"SyncWorker Id: \" + syncRequest.getId());\n//        }\n\n        Constraints.Builder builder = new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED);\n        PeriodicWorkRequest syncRequest = new PeriodicWorkRequest.Builder(SyncWorker.class, App.i().getUser().getAutoSyncFrequency(), TimeUnit.MINUTES)\n                .setConstraints(builder.build())\n                .addTag(SyncWorker.TAG)\n                .build();\n        WorkManager.getInstance(this).enqueueUniquePeriodicWork(SyncWorker.TAG, ExistingPeriodicWorkPolicy.KEEP,syncRequest);\n        KLog.i(\"SyncWorker Id: \" + syncRequest.getId());\n\n        if(App.i().getUser().isAutoSync()){\n        }\n\n        LiveEventBus.get(SyncWorker.SYNC_TASK_STATUS,Boolean.class)\n                .observeSticky(this, new Observer<Boolean>() {\n                    @Override\n                    public void onChanged(Boolean isSyncing) {\n                        KLog.e(\"任务状态：\"  + isSyncing );\n                        swipeRefreshLayoutS.setRefreshing(false);\n                        if(isSyncing){\n                            swipeRefreshLayoutS.setEnabled(false);\n                        }else {\n                            swipeRefreshLayoutS.setEnabled(true);\n                        }\n                    }\n                });\n        LiveEventBus\n                .get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE, String.class)\n                .observe(this, new Observer<String>() {\n                    @Override\n                    public void onChanged(@Nullable String tips) {\n                        toolbar.setSubtitle( tips );\n                    }\n                });\n        LiveEventBus.get(SyncWorker.NEW_ARTICLE_NUMBER,Integer.class)\n                .observe(this, new Observer<Integer>() {\n                    @Override\n                    public void onChanged(Integer integer) {\n                        if(integer == 0){\n                            return;\n                        }\n                        SnackbarUtil.Long(articleListView,bottomBar, getResources().getQuantityString(R.plurals.has_new_articles,integer,integer) )\n                                .setAction(getString(R.string.view), new View.OnClickListener() {\n                                    @Override\n                                    public void onClick(View v) {\n                                        refreshData();\n                                    }\n                                }).show();\n                        refreshIcon.setVisibility(View.VISIBLE);\n                    }\n                });\n    }\n\n\n    private void applyPermissions() {\n        XXPermissions.with(this)\n                //.constantRequest() //可设置被拒绝后继续申请，直到用户授权或者永久拒绝\n                //.permission(Permission.SYSTEM_ALERT_WINDOW) //支持请求6.0悬浮窗权限8.0请求安装权限\n                .permission(Permission.Group.STORAGE) //不指定权限则自动获取清单中的危险权限\n                .request(new OnPermission() {\n                    @Override\n                    public void hasPermission(List<String> granted, boolean isAll) {\n                    }\n\n                    @Override\n                    public void noPermission(List<String> denied, boolean quick) {\n                        for (String id : denied) {\n                            KLog.e(\"无法获取权限\" + id);\n                        }\n                        ToastUtils.show(getString(R.string.plz_grant_permission_tips));\n                    }\n                });\n    }\n\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n        if (articlesAdapter != null) {\n            articlesAdapter.notifyDataSetChanged();\n        }\n    }\n\n    private void showAutoSwitchThemeSnackBar() {\n        if (!App.i().getUser().isAutoToggleTheme()) {\n            return;\n        }\n        int hour = TimeUtil.getCurrentHour();\n        int themeMode;\n        if (hour >= 7 && hour < 20) {\n            themeMode = App.THEME_DAY;\n        } else {\n            themeMode = App.THEME_NIGHT;\n        }\n        if (App.i().getUser().getThemeMode() == themeMode) {\n            return;\n        }\n\n        SnackbarUtil.Long(articleListView, bottomBar, getString(R.string.theme_switched_automatically))\n                .setAction(getString(R.string.cancel), new View.OnClickListener() {\n                    @Override\n                    public void onClick(View v) {\n                        manualToggleTheme();\n                    }\n                }).show();\n    }\n\n\n    protected void initIconView() {\n        vToolbarAutoMark = findViewById(R.id.main_toolbar_auto_mark);\n        if (App.i().getUser().isMarkReadOnScroll()) {\n            vToolbarAutoMark.setVisibility(View.VISIBLE);\n        }\n        vPlaceHolder = findViewById(R.id.main_placeholder);\n        refreshIcon = findViewById(R.id.main_bottombar_refresh_articles);\n    }\n\n    public void clickSearchIcon(View view) {\n        Intent intent = new Intent(MainActivity.this, SearchActivity.class);\n        startActivityForResult(intent, 0);\n        overridePendingTransition(R.anim.in_from_bottom, R.anim.fade_out);\n    }\n\n    protected void initSwipeRefreshLayout() {\n        swipeRefreshLayoutS = findViewById(R.id.main_swipe_refresh);\n        if (swipeRefreshLayoutS == null) {\n            return;\n        }\n        swipeRefreshLayoutS.setOnRefreshListener(this);\n        //设置样式刷新显示的位置\n        swipeRefreshLayoutS.setProgressViewOffset(true, 0, 120);\n        swipeRefreshLayoutS.setViewGroup(articleListView);\n    }\n\n    @Override\n    public void onRefresh() {\n        if (!swipeRefreshLayoutS.isEnabled()) {\n            return;\n        }\n        KLog.i(\"【刷新中】\");\n        Constraints.Builder builder = new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED);\n        OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(SyncWorker.class)\n                .setConstraints(builder.build())\n                .addTag(SyncWorker.TAG)\n                .build();\n        WorkManager.getInstance(this).enqueue(oneTimeWorkRequest);\n    }\n\n    // 按下back键时会调用onDestroy()销毁当前的activity，重新启动此activity时会调用onCreate()重建；\n    // 而按下home键时会调用onStop()方法，并不销毁activity，重新启动时则是调用onResume()\n    @Override\n    protected void onDestroy() {\n        // 参数为null，会将所有的Callbacks和Messages全部清除掉。\n        // 这样做的好处是在Activity退出的时候，可以避免内存泄露。因为 handler 内可能引用 Activity\n        maHandler.removeCallbacksAndMessages(null);\n        //EventBus.getDefault().unregister(this);\n        super.onDestroy();\n    }\n\n    /**\n     * App.StreamState 包含 3 个状态：All，Unread，Stared\n     * App.streamId 至少包含 1 个状态： Reading-list\n     */\n    protected void refreshData() { // 获取 App.articleList , 并且根据 App.articleList 的到未读数目\n        //KLog.e(\"refreshData：\" + App.i().getUser().getStreamId() + \" = \" + App.i().getUser().getStreamStatus() + \"   \" + App.i().getUser().getUserId());\n        getArtData();\n        refreshIcon.setVisibility(View.GONE);\n    }\n\n\n    private ArticleViewModel articleViewModel;\n    private void getArtData() {\n        String uid = App.i().getUser().getId();\n        int streamStatus = App.i().getUser().getStreamStatus();\n        int streamType = App.i().getUser().getStreamType();\n        String streamId = App.i().getUser().getStreamId();\n        articlesAdapter.setLastPos(linearLayoutManager.findLastVisibleItemPosition()-1);\n\n        if(articleViewModel.articles != null && articleViewModel.articles.hasObservers()){\n            articleViewModel.articles.removeObservers(this);\n            articleViewModel.articles = null;\n        }\n        articleViewModel.getArticles(uid,streamId,streamType,streamStatus).observe(this, new Observer<PagedList<Article>>() {\n            @Override\n            public void onChanged(PagedList<Article> articles) {\n//                if( articlesAdapter.getCurrentList() != null ){\n//                    KLog.e(\"更新列表数据 A : \" + articlesAdapter.getCurrentList().getLastKey()  +  \" , \" + (linearLayoutManager.findLastVisibleItemPosition()-1) );\n//                }\n//                if( articlesAdapter.getCurrentList() != null ){\n//                    KLog.e(\"更新列表数据 B : \" + articlesAdapter.getCurrentList().getLastKey()  +  \" , \" + (linearLayoutManager.findLastVisibleItemPosition()-1) );\n//                }\n                articlesAdapter.submitList(articles);\n//                if( articlesAdapter.getCurrentList() != null ){\n//                    KLog.e(\"更新列表数据 C : \" + articlesAdapter.getCurrentList().getLastKey()  +  \" , \" + (linearLayoutManager.findLastVisibleItemPosition()-1) );\n//                }\n                loadViewByData( articles.size() );\n            }\n        });\n        articleListView.scrollToPosition(0);\n        // KLog.e(\"更新列表数据 A  , \" + articlesAdapter.getItemCount() );\n    }\n\n    private void loadViewByData(int size) {\n        // 在setSupportActionBar(toolbar)之后调用toolbar.setTitle()的话。 在onCreate()中调用无效。在onStart()中调用无效。 在onResume()中调用有效。\n        getSupportActionBar().setTitle(App.i().getUser().getStreamTitle());\n        countTips.setText( getResources().getQuantityString(R.plurals.articles_count, size, size ) );\n\n//        KLog.i(\"【loadViewByData】\" + App.i().getUser().getStreamId()+ \"--\" + App.i().getUser().getStreamTitle() + \"--\" + App.i().getUser().getStreamStatus() + \"--\" + toolbar.getTitle() + articlesAdapter.getItemCount());\n        if (articlesAdapter == null || articlesAdapter.getItemCount() == 0) {\n            vPlaceHolder.setVisibility(View.VISIBLE);\n            articleListView.setVisibility(View.GONE);\n        } else {\n            vPlaceHolder.setVisibility(View.GONE);\n            articleListView.setVisibility(View.VISIBLE);\n        }\n    }\n\n    public void showTagDialog(final Collection category) {\n        // 重命名弹窗的适配器\n        MaterialSimpleListAdapter adapter = new MaterialSimpleListAdapter(new MaterialSimpleListAdapter.Callback() {\n            @Override\n            public void onMaterialListItemSelected(MaterialDialog dialog, int index, MaterialSimpleListItem item) {\n                if (index == 0) {\n                    new MaterialDialog.Builder(MainActivity.this)\n                            .title(R.string.edit_name)\n                            .inputType(InputType.TYPE_CLASS_TEXT)\n                            .inputRange(1, 22)\n                            .input(null, category.getTitle(), new MaterialDialog.InputCallback() {\n                                @Override\n                                public void onInput(@NotNull MaterialDialog dialog, CharSequence input) {\n                                    renameTag(input.toString(), category);\n                                }\n                            })\n                            .positiveText(R.string.confirm)\n                            .negativeText(android.R.string.cancel)\n                            .show();\n                }\n                dialog.dismiss();\n            }\n        });\n        adapter.add(new com.afollestad.materialdialogs.simplelist.MaterialSimpleListItem.Builder(MainActivity.this)\n                .content(R.string.rename)\n                .icon(R.drawable.ic_rename)\n                .backgroundColor(Color.TRANSPARENT)\n                .build());\n\n        new MaterialDialog.Builder(MainActivity.this)\n                .adapter(adapter, new LinearLayoutManager(MainActivity.this))\n                .show();\n    }\n\n    public void renameTag(final String renamedTagTitle, Collection category) {\n        KLog.i(\"renameTag\", renamedTagTitle);\n        if (renamedTagTitle.equals(\"\") || category.getTitle().equals(renamedTagTitle)) {\n            return;\n        }\n        final String newCategoryId = \"user/\" + App.i().getUser().getUserId() + \"/\" + renamedTagTitle;\n        final String oldCategoryId = category.getId();\n        App.i().getApi().renameTag(oldCategoryId, renamedTagTitle, new CallbackX<String,String>() {\n            @Override\n            public void onSuccess(String result) {\n                CoreDB.i().categoryDao().updateId(App.i().getUser().getId(),oldCategoryId,newCategoryId);\n                CoreDB.i().feedCategoryDao().updateCategoryId(App.i().getUser().getId(),oldCategoryId,newCategoryId);\n                tagListAdapter.notifyDataSetChanged();\n            }\n\n            @Override\n            public void onFailure(String error) {\n                ToastUtils.show(getString(R.string.rename_failed));\n            }\n        });\n    }\n\n    public void showFeedActivity(final int parentPosition, final int childPosition) {\n        Collection feed = tagListAdapter.getChild(parentPosition, childPosition);\n        if (feed == null) {\n            return;\n        }\n        Intent intent = new Intent(MainActivity.this, FeedActivity.class);\n        intent.putExtra(\"feedId\", feed.getId());\n        startActivity(intent);\n    }\n\n\n    LinearLayoutManager linearLayoutManager;\n    public void initArtListView() {\n        articleListView = findViewById(R.id.main_slv);\n        linearLayoutManager = new LinearLayoutManager(this);\n        articleListView.setLayoutManager(linearLayoutManager);\n\n        // HeaderView。\n        articlesHeaderView = getLayoutInflater().inflate(R.layout.main_item_header, articleListView, false);\n        countTips = (TextView) articlesHeaderView.findViewById(R.id.main_header_title);\n        ImageView eye = articlesHeaderView.findViewById(R.id.main_header_eye);\n        eye.setOnClickListener(v -> ToastUtils.show(R.string.display_filter_in_development) );\n        articleListView.addHeaderView(articlesHeaderView);\n\n        articleListView.setOnItemLongClickListener(new OnItemLongClickListener() {\n            @Override\n            public void onItemLongClick(View view, final int position) {\n                new XPopup.Builder(MainActivity.this)\n                        .isCenterHorizontal(true) //是否与目标水平居中对齐\n                        .offsetY(-view.getHeight() / 2)\n                        .hasShadowBg(true)\n                        .popupAnimation(PopupAnimation.ScaleAlphaFromCenter)\n                        .atView(view)  // 依附于所点击的View，内部会自动判断在上方或者下方显示\n                        .asAttachList(\n                                new String[]{getString(R.string.speak_article), getString(R.string.mark_up), getString(R.string.mark_down), getString(R.string.mark_unread)},\n                                new int[]{R.drawable.ic_volume, R.drawable.ic_mark_up, R.drawable.ic_mark_down, R.drawable.ic_mark_unread},\n                                new OnSelectListener() {\n                                    @Override\n                                    public void onSelect(int which, String text) {\n                                        switch (which) {\n                                            case 0:\n                                                Intent intent = new Intent(MainActivity.this,TTSActivity.class);\n                                                intent.putExtra(\"articleNo\",position);\n                                                intent.putExtra(\"isQueue\",true);\n                                                startActivity(intent);\n                                                break;\n                                            case 1:\n                                                Integer[] index = new Integer[2];\n                                                index[0] = position + 1;\n                                                index[1] = 0;\n                                                new MarkListReadedAsyncTask().execute(index);\n                                                break;\n                                            case 2:\n                                                showConfirmDialog(position,articlesAdapter.getItemCount());\n                                                break;\n                                            case 3:\n                                                Article article = articlesAdapter.getItem(position);\n//                                                Article article = CoreDB.i().articleDao().getById(App.i().getUser().getId(),articlesAdapter.getItem(position).getId());\n                                                if( article == null ){\n                                                    return;\n                                                }\n\n                                                if (article.getReadStatus() == App.STATUS_READED) {\n                                                    int oldReadStatus = article.getReadStatus();\n                                                    App.i().getApi().markArticleUnread(article.getId(), new CallbackX() {\n                                                        @Override\n                                                        public void onSuccess(Object result) {\n                                                        }\n                                                        @Override\n                                                        public void onFailure(Object error) {\n                                                            article.setReadStatus(oldReadStatus);\n                                                            CoreDB.i().articleDao().update(article);\n                                                        }\n                                                    });\n                                                }\n                                                article.setReadStatus(App.STATUS_UNREADING);\n                                                CoreDB.i().articleDao().update(article);\n                                                articlesAdapter.notifyItemChanged(position);\n                                                break;\n                                            default:\n                                                break;\n                                        }\n                                    }\n                                })\n                        .show();\n            }\n        });\n\n        articleListView.addOnScrollListener(new RecyclerView.OnScrollListener() {\n            // 正在被外部拖拽,一般为用户正在用手指滚动 SCROLL_STATE_DRAGGING，自动滚动 SCROLL_STATE_SETTLING，正在滚动（SCROLL_STATE_IDLE）\n            @Override\n            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {\n                super.onScrollStateChanged(recyclerView, newState);\n                articlesAdapter.setLastPos(linearLayoutManager.findLastVisibleItemPosition()-1);\n                //KLog.i(\"【滚动】\" + ((RecyclerView.LayoutParams) recyclerView.getChildAt(1).getLayoutParams()).getViewAdapterPosition() + \" = \"+  linearLayoutManager.findFirstVisibleItemPosition() + \" , \" + linearLayoutManager.findLastVisibleItemPosition());\n                if (!autoMarkReaded) {\n                    return;\n                }\n                //  || RecyclerView.SCROLL_STATE_SETTLING == newState\n                //KLog.i(\"滚动：\" + newState);\n                if (RecyclerView.SCROLL_STATE_DRAGGING == newState && scrollIndex == null) {\n                    scrollIndex = new Integer[2];\n                    scrollIndex[0] = ((RecyclerView.LayoutParams) recyclerView.getChildAt(0).getLayoutParams()).getViewAdapterPosition();\n                    KLog.i(\"滚动开始：\" + scrollIndex[0] + \" = \"+  linearLayoutManager.findFirstVisibleItemPosition() );\n                } else if (RecyclerView.SCROLL_STATE_IDLE == newState && scrollIndex != null) {\n                    scrollIndex[1] = ((RecyclerView.LayoutParams) recyclerView.getChildAt(0).getLayoutParams()).getViewAdapterPosition();\n                    new MarkListReadedAsyncTask().execute(scrollIndex);\n                    KLog.i(\"滚动结束：\" + scrollIndex[1]  + \" = \"+  linearLayoutManager.findFirstVisibleItemPosition() );\n                    scrollIndex = null;\n                }\n            }\n        });\n\n        // 创建菜单：\n\n        SwipeMenuCreator mSwipeMenuCreator = new SwipeMenuCreator() {\n            @Override\n            public void onCreateMenu(SwipeMenu leftMenu, SwipeMenu rightMenu, int position) {\n                Article article = articlesAdapter.getItem(position);\n//                Article article = CoreDB.i().articleDao().getById(App.i().getUser().getId(),articlesAdapter.getArticleId(position));\n                if(article==null){\n                    //KLog.i(\"文章数据为null: \" + position  + \" , \" +  articlesAdapter.getCurrentList().getLastKey() + \" , \" + articlesAdapter.getCurrentList().getLoadedCount()  );\n                    return;\n                }\n//                else {\n//                    KLog.i(\"文章数据为: \" + position + \" , \" + articlesAdapter.getCurrentList().getLoadedCount() + \" , \" +  articlesAdapter.getCurrentList().getLastKey() );\n//                }\n\n                int width = getResources().getDimensionPixelSize(R.dimen.dp_80);\n                int margin = getResources().getDimensionPixelSize(R.dimen.dp_30);\n\n//                KLog.e(\"添加左右菜单\" + position );\n                // 1. MATCH_PARENT 自适应高度，保持和Item一样高;  2. 指定具体的高，比如80; 3. WRAP_CONTENT，自身高度，不推荐;\n                int height = ViewGroup.LayoutParams.MATCH_PARENT;\n\n                SwipeMenuItem starItem = new SwipeMenuItem(MainActivity.this); // 各种文字和图标属性设置。\n                if (article.getStarStatus() == App.STATUS_STARED) {\n                    starItem.setImage(R.drawable.ic_state_unstar);\n                } else {\n                    starItem.setImage(R.drawable.ic_state_star);\n                }\n                starItem.setWeight(width);\n                starItem.setHeight(height);\n                starItem.setMargins(margin, 0, margin, 0);\n                leftMenu.addMenuItem(starItem); // 在Item左侧添加一个菜单。\n\n                SwipeMenuItem readItem = new SwipeMenuItem(MainActivity.this); // 各种文字和图标属性设置。\n                if (article.getReadStatus() == App.STATUS_READED) {\n                    readItem.setImage(R.drawable.ic_state_unread);\n                } else {\n                    readItem.setImage(R.drawable.ic_read);\n                }\n                readItem.setWeight(width);\n                readItem.setHeight(height);\n                readItem.setMargins(margin, 0, margin, 0);\n                rightMenu.addMenuItem(readItem); // 在Item右侧添加一个菜单。\n                // 注意：哪边不想要菜单，那么不要添加即可。\n            }\n        };\n        articleListView.setSwipeMenuCreator(mSwipeMenuCreator);\n\n        articleListView.setOnItemSwipeListener(new OnItemSwipeListener() {\n            @Override\n            public void onClose(View swipeMenu, int direction, int adapterPosition) {\n            }\n\n            @Override\n            public void onCloseLeft(int position) {\n                KLog.i(\"onCloseLeft：\" + position + \"  \");\n                toggleStarState(position);\n            }\n\n            @Override\n            public void onCloseRight(int position) {\n                KLog.i(\"onCloseRight：\" + position + \"  \");\n                toggleReadState(position);\n            }\n        });\n\n        OnItemMenuClickListener mItemMenuClickListener = new OnItemMenuClickListener() {\n            @Override\n            public void onItemClick(SwipeMenuBridge menuBridge, int position) {\n                // 任何操作必须先关闭菜单，否则可能出现Item菜单打开状态错乱。\n                menuBridge.closeMenu();\n\n                // 左侧还是右侧菜单：\n                int direction = menuBridge.getDirection();\n\n                if (direction == SwipeRecyclerView.RIGHT_DIRECTION) {\n                    KLog.i(\"onItemClick  onCloseRight：\" + position + \"  \");\n                    if (position > -1) {\n                        toggleReadState(position);\n                    }\n                } else if (direction == SwipeRecyclerView.LEFT_DIRECTION) {\n                    KLog.i(\"onItemClick  onCloseLeft：\" + position + \"  \");\n                    if (position > -1) {\n                        toggleStarState(position);\n                    }\n                }\n            }\n        };\n        // 菜单点击监听。\n        articleListView.setOnItemMenuClickListener(mItemMenuClickListener);\n\n        articleListView.setOnItemClickListener(new OnItemClickListener() {\n            @Override\n            public void onItemClick(View view, int position) {\n                if (position < 0) {\n                    return;\n                }\n                Intent intent = new Intent(MainActivity.this, ArticleActivity.class);\n                intent.putExtra(\"theme\", App.i().getUser().getThemeMode());\n\n                String articleId = articlesAdapter.getItem(position).getId();\n                //String articleId = articlesAdapter.getArticleId(position);\n\n                intent.putExtra(\"articleId\", articleId);\n                // 下标从 0 开始\n                intent.putExtra(\"articleNo\", position);\n                intent.putExtra(\"articleCount\", articlesAdapter.getItemCount());\n                startActivityForResult(intent, 0);\n                overridePendingTransition(R.anim.in_from_bottom, R.anim.fade_out);\n\n                //KLog.i(\"点击了\" + articleID + \"，位置：\" + position + \"，文章ID：\" + articleID + \"    \" + App.articleList.size());\n            }\n        });\n        articleViewModel = new ViewModelProvider(this).get(ArticleViewModel.class);\n        articlesAdapter = new ArticlePagedListAdapter();\n        articleListView.setAdapter(articlesAdapter);\n        App.i().articlesAdapter = articlesAdapter;\n    }\n\n\n//    private CategoryViewModel categoryViewModel;\n//    public void onTagIconClicked(View view) {\n//        KLog.i(\"tag按钮被点击\");\n//        tagBottomSheetDialog.show();\n//        if(categoryViewModel == null){\n//            categoryViewModel = new ViewModelProvider(this).get(CategoryViewModel.class);\n//        }\n//        categoryViewModel.getCategoriesLiveData().observe(this, new Observer<List<Category>>() {\n//            @Override\n//            public void onChanged(List<Category> categories) {\n//                String userId = App.i().getUser().getUserId();\n//                // 总分类\n//                Category rootCategory = new Category();\n//                rootCategory.setTitle(getString(R.string.all));\n//                int unreadCount = CoreDB.i().articleDao().getUnreadCount(App.i().getUser().getId());\n//                rootCategory.setUnreadCount(unreadCount);\n//                rootCategory.setId(\"user/\" + userId + App.CATEGORY_ALL);\n//\n//                // 未分类\n//                Category unCategory = new Category();\n//                unCategory.setTitle(getString(R.string.un_category));\n//                int unreadUnCategoryCount = CoreDB.i().articleDao().getUnreadUncategoryCount(App.i().getUser().getId());\n//                unCategory.setUnreadCount(unreadUnCategoryCount);\n//                unCategory.setId(\"user/\" + userId + App.CATEGORY_UNCATEGORIZED);\n//\n//                categories.add(0,rootCategory);\n//                categories.add(1,unCategory);\n//                tagListAdapter.setParents(categories);\n//                tagListAdapter.notifyDataChanged();\n////                CategoryDiffCallback callback = new CategoryDiffCallback(tagListAdapter.getCategories(), categories);\n////                //对比数据\n////                DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback);\n////                //然后刷新，完事儿\n////                result.dispatchUpdatesTo(tagListAdapter);\n//            }\n//        });\n//    }\n\n    public void onClickCategoryIcon(View view) {\n        tagBottomSheetDialog.show();\n        AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {\n            @Override\n            public void run() {\n                String uid = App.i().getUser().getUserId();\n\n                // 总分类\n                Collection rootCategory = new Collection();\n                rootCategory.setTitle(getString(R.string.all));\n                int unreadCount = CoreDB.i().articleDao().getUnreadCount(App.i().getUser().getId());\n                rootCategory.setId(\"user/\" + uid + App.CATEGORY_ALL);\n\n                // 未分类\n                Collection unCategory = new Collection();\n                unCategory.setTitle(getString(R.string.un_category));\n                int unreadUnCategoryCount = CoreDB.i().articleDao().getUncategoryUnreadCount(App.i().getUser().getId());\n                unCategory.setId(\"user/\" + uid + App.CATEGORY_UNCATEGORIZED);\n\n                // 已分类\n                List<Collection> categories;\n\n                if( App.i().getUser().getStreamStatus() == App.STATUS_UNREAD ){\n                    rootCategory.setCount(CoreDB.i().articleDao().getUnreadCount(App.i().getUser().getId()));\n                    unCategory.setCount(CoreDB.i().articleDao().getUncategoryUnreadCount(App.i().getUser().getId()));\n                    categories = CoreDB.i().categoryDao().getCategoriesUnreadCount(App.i().getUser().getId());\n                }else if( App.i().getUser().getStreamStatus() == App.STATUS_STARED ){\n                    rootCategory.setCount(CoreDB.i().articleDao().getStarCount(App.i().getUser().getId()));\n                    unCategory.setCount(CoreDB.i().articleDao().getUncategoryStarCount(App.i().getUser().getId()));\n                    categories = CoreDB.i().categoryDao().getCategoriesStarCount(App.i().getUser().getId());\n                }else {\n                    rootCategory.setCount(CoreDB.i().articleDao().getAllCount(App.i().getUser().getId()));\n                    unCategory.setCount(CoreDB.i().articleDao().getUncategoryAllCount(App.i().getUser().getId()));\n                    categories = CoreDB.i().categoryDao().getCategoriesAllCount(App.i().getUser().getId());\n                }\n\n                List<Collection> categoryListTemp = new ArrayList<>();\n                categoryListTemp.add(rootCategory);\n                categoryListTemp.add(unCategory);\n                // 数据库中的所有分类\n                categoryListTemp.addAll(categories);\n                runOnUiThread(new Runnable() {\n                    @Override\n                    public void run() {\n                        tagListAdapter.setParents(categoryListTemp);\n                        tagListAdapter.notifyDataChanged();\n                        KLog.i(\"tag按钮被点击\");\n                    }\n                });\n            }\n        });\n    }\n\n//    public void onClickCategoryIcon2(View view) {\n//        tagBottomSheetDialog.show();\n//        AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {\n//            @Override\n//            public void run() {\n//                String uid = App.i().getUser().getUserId();\n//\n//                // 总分类\n//                Category rootCategory = new Category();\n//                rootCategory.setTitle(getString(R.string.all));\n//                int unreadCount = CoreDB.i().articleDao().getUnreadCount(App.i().getUser().getId());\n//                rootCategory.setUnreadCount(unreadCount);\n//                rootCategory.setId(\"user/\" + uid + App.CATEGORY_ALL);\n//\n//                // 未分类\n//                Category unCategory = new Category();\n//                unCategory.setTitle(getString(R.string.un_category));\n//                int unreadUnCategoryCount = CoreDB.i().articleDao().getUnreadUncategoryCount(App.i().getUser().getId());\n//                unCategory.setUnreadCount(unreadUnCategoryCount);\n//                unCategory.setId(\"user/\" + uid + App.CATEGORY_UNCATEGORIZED);\n//\n//                List<Category> categoryListTemp = new ArrayList<>();\n//                categoryListTemp.add(rootCategory);\n//                categoryListTemp.add(unCategory);\n////                categoryListTemp.add(tagCategory);\n//\n//                List<Category> categories;\n//                categories = CoreDB.i().categoryDao().getAll(App.i().getUser().getId());\n//\n//                // 数据库中的所有分类\n//                categoryListTemp.addAll(categories);\n//                runOnUiThread(new Runnable() {\n//                    @Override\n//                    public void run() {\n//                        tagListAdapter.setParents(categoryListTemp);\n//                        tagListAdapter.notifyDataChanged();\n//                        KLog.i(\"tag按钮被点击\");\n//                    }\n//                });\n//            }\n//        });\n//    }\n\n    public void initTagListView() {\n        tagBottomSheetDialog = new BottomSheetDialog(MainActivity.this);\n        tagBottomSheetDialog.setContentView(R.layout.bottom_sheet_category);\n        relativeLayout = tagBottomSheetDialog.findViewById(R.id.sheet_tag);\n\n        IconFontView iconFontView = tagBottomSheetDialog.findViewById(R.id.main_tag_close);\n        iconFontView.setOnClickListener(view -> {\n            tagBottomSheetDialog.dismiss();\n            // iconFontView.setVisibility(View.GONE);\n        });\n\n        tagListView = tagBottomSheetDialog.findViewById(R.id.main_tag_list_view);\n\n        tagListView.setLayoutManager(new LinearLayoutManager(this));\n        // 还有另外一种方案，通过设置动画执行时间为0来解决问题：\n        tagListView.getItemAnimator().setChangeDuration(0);\n        // 关闭默认的动画\n        ((SimpleItemAnimator) tagListView.getItemAnimator()).setSupportsChangeAnimations(false);\n\n        //stickyHeaderLayout = tagBottomSheetDialog.findViewById(R.id.sticky_header_layout);\n        //headerPinnedView = getLayoutInflater().inflate(R.layout.tag_expandable_item_group_header, stickyHeaderLayout, false);\n        //stickyHeaderLayout.setStickyHeaderView(headerPinnedView);\n\n        tagListAdapter = new ExpandedAdapter(this);\n        tagListView.setOnItemClickListener(new OnItemClickListener() {\n            @Override\n            public void onItemClick(View view, int adapterPosition) {\n                tagBottomSheetDialog.dismiss();\n                // 根据原position判断该item是否是parent item\n                int groupPosition = tagListAdapter.parentItemPosition(adapterPosition);\n                User user = App.i().getUser();\n                if (tagListAdapter.isParentItem(adapterPosition)) {\n                    App.i().getUser().setStreamId( tagListAdapter.getGroup(groupPosition).getId().replace(\"\\\"\", \"\")  );\n                    App.i().getUser().setStreamTitle( tagListAdapter.getGroup(groupPosition).getTitle() );\n                    App.i().getUser().setStreamType( App.TYPE_GROUP );\n                    //KLog.i(\"【 TagList 被点击】\" + App.i().getUser().toString());\n                    refreshData();\n                } else {\n                    int childPosition = tagListAdapter.childItemPosition(adapterPosition);\n                    Collection feed = tagListAdapter.getChild(groupPosition, childPosition);\n                    App.i().getUser().setStreamId( feed.getId() );\n                    App.i().getUser().setStreamTitle( feed.getTitle() );\n                    App.i().getUser().setStreamType( App.TYPE_FEED );\n                    refreshData();\n                }\n                CoreDB.i().userDao().update(user);\n            }\n        });\n\n        tagListView.setOnItemLongClickListener(new OnItemLongClickListener() {\n            @Override\n            public void onItemLongClick(View view, int adapterPosition) {\n                // KLog.e(\"被长安，view的id是\" + allArticleHeaderView.getId() + \"，parent的id\" + parent.getId() + \"，Tag是\" + allArticleHeaderView.getCategoryById() + \"，位置是\" + tagListView.getPositionForView(allArticleHeaderView));\n                // 根据原position判断该item是否是parent item\n                if (tagListAdapter.isParentItem(adapterPosition)) {\n                    int parentPosition = tagListAdapter.parentItemPosition(adapterPosition);\n                    showTagDialog(tagListAdapter.getGroup(parentPosition));\n                } else {\n                    // 换取child position\n                    int parentPosition = tagListAdapter.parentItemPosition(adapterPosition);\n                    int childPosition = tagListAdapter.childItemPosition(adapterPosition);\n                    showFeedActivity(parentPosition, childPosition);\n                }\n            }\n        });\n        tagListView.setAdapter(tagListAdapter);\n    }\n\n    private void showConfirmDialog(final int start, final int end) {\n        new AlertDialog.Builder(MainActivity.this)\n                .setMessage(R.string.main_dialog_confirm_mark_article_list)\n                .setPositiveButton(R.string.confirm, (dialog, which) -> {\n                    Integer[] index = new Integer[2];\n                    index[0] = start;\n                    index[1] = end;\n                    new MarkListReadedAsyncTask().execute(index);\n                })\n                .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialog, int which) {\n                        dialog.dismiss();\n                    }\n                })\n                .show();\n    }\n\n\n    // 标记以上/以下为已读\n    @SuppressLint(\"StaticFieldLeak\")\n    private class MarkListReadedAsyncTask extends AsyncTask<Integer, Integer, Integer> {\n        private List<Article> articleList;\n        private List<String> articleIDs;\n        private void handleArticle(int i){\n            try {\n                int retry = 0;\n                Article article = articlesAdapter.getItem(i);\n                if( article == null ){\n                    articlesAdapter.load(i);\n                    do {\n                        //KLog.i(\"文章为空：\" + i );\n                        Thread.sleep(500);\n                        retry ++;\n                        article = articlesAdapter.getItem(i);\n                    }while ( article == null && retry < 3 );\n                    //KLog.e(\"文章是否为空：\" + (article==null)  + \"   ,  \" + (articlesAdapter.getItem(i)==null) );\n                    if( article == null ){ return; }\n                }\n\n                articlesAdapter.setLastItem(linearLayoutManager.findLastVisibleItemPosition()-1);\n                if (article.getReadStatus() == App.STATUS_UNREAD) {\n                    article.setReadStatus(App.STATUS_READED);\n                    articleList.add(article);\n                    articleIDs.add(article.getId());\n                    //提交之后，会执行onProcessUpdate方法，通知对应这个item更新界面\n                    publishProgress(i);\n                }\n            } catch (IllegalStateException | InterruptedException e) {\n                KLog.e(\"获取数据错误\");\n                e.printStackTrace();\n            }\n        }\n        @Override\n        protected Integer doInBackground(Integer... params) {\n            int startIndex, endIndex;\n            boolean desc;\n            desc = params[0] >= params[1];\n            startIndex = params[0];\n            endIndex = params[1];\n\n            if( desc ){\n                articleList = new ArrayList<>(startIndex - endIndex);\n                articleIDs = new ArrayList<>(startIndex - endIndex);\n                for (int i = startIndex - 1; i >= endIndex; i--){\n                    handleArticle(i);\n                }\n            }else {\n                articleList = new ArrayList<>(endIndex - startIndex);\n                articleIDs = new ArrayList<>(endIndex - startIndex);\n                for (int i = startIndex; i < endIndex; i++){\n                    handleArticle(i);\n                }\n            }\n\n            if (articleIDs.size() == 0) {\n                return 0;\n            }\n            if (desc) {\n                Collections.reverse(articleList);\n                Collections.reverse(articleIDs);\n            }\n\n            int needCount = articleIDs.size();\n            int hadCount = 0;\n            int num = 0;\n\n            while (needCount > 0) {\n                num = Math.min(100, needCount);\n                List<Article> subArticles = articleList.subList(hadCount, hadCount + num);\n                List<String> subArticleIDs = articleIDs.subList(hadCount, hadCount + num);\n                hadCount = hadCount + num;\n                CoreDB.i().articleDao().update(subArticles);\n                App.i().getApi().markArticleListReaded(subArticleIDs, new CallbackX() {\n                    @Override\n                    public void onSuccess(Object result) {\n                    }\n\n                    @Override\n                    public void onFailure(Object error) {\n                        for (Article article: subArticles) {\n                            article.setReadStatus(App.STATUS_UNREAD);\n                        }\n                        CoreDB.i().articleDao().update(subArticles);\n                    }\n                });\n\n                needCount = articleIDs.size() - hadCount;\n            }\n\n            //返回结果\n            return 0;\n        }\n\n//        /**\n//         * 在调用cancel方法后会执行到这里\n//         */\n//        @Override\n//        protected void onCancelled() {\n//        }\n//\n//        /**\n//         * 在doInbackground之后执行\n//         */\n//        @Override\n//        protected void onPostExecute(Integer args3) {\n//        }\n//\n//        /**\n//         * 在doInBackground之前执行\n//         */\n//        @Override\n//        protected void onPreExecute() {\n//        }\n\n        /**\n         * 特别赞一下这个多次参数的方法，特别方便\n         *\n         * @param progress\n         */\n        @Override\n        protected void onProgressUpdate(Integer... progress) {\n            KLog.e(\"更新进度\" + progress[0] );\n            // 应该是去通知对应的那个 item 改变。\n            articlesAdapter.notifyItemChanged(progress[0]);\n        }\n    }\n\n    private void showSearchResult(String keyword) {\n        User user = App.i().getUser();\n        user.setStreamId(App.CATEGORY_SEARCH);\n        user.setStreamTitle(getString(R.string.main_toolbar_title_search) + keyword);\n        CoreDB.i().userDao().update(user);\n\n        if(articleViewModel.articles != null && articleViewModel.articles.hasObservers()){\n            articleViewModel.articles.removeObservers(this);\n            articleViewModel.articles = null;\n        }\n        articleViewModel.getAllByKeyword(App.i().getUser().getId(),keyword).observe(this, new Observer<PagedList<Article>>() {\n            @Override\n            public void onChanged(PagedList<Article> articles) {\n                articlesAdapter.submitList(articles);\n                loadViewByData( articles.size() );\n            }\n        });\n        articleListView.scrollToPosition(0);\n    }\n\n\n    private void toggleReadState(final int position) {\n        if (position < 0) {\n            return;\n        }\n        // String articleId = articlesAdapter.getItem(position).getId();\n        // Article article = CoreDB.i().articleDao().getById(App.i().getUser().getId(),articleId);\n        Article article = articlesAdapter.getItem(position);\n        if (autoMarkReaded && article.getReadStatus() == App.STATUS_UNREAD) {\n            article.setReadStatus(App.STATUS_UNREADING);\n            CoreDB.i().articleDao().update(article);\n        } else if (article.getReadStatus() == App.STATUS_READED) {\n            article.setReadStatus(App.STATUS_UNREADING);\n            CoreDB.i().articleDao().update(article);\n            App.i().getApi().markArticleUnread(article.getId(), new CallbackX() {\n                @Override\n                public void onSuccess(Object result) {\n                }\n\n                @Override\n                public void onFailure(Object error) {\n                    article.setReadStatus(App.STATUS_READED);\n                    CoreDB.i().articleDao().update(article);\n                    KLog.e(\"失败的原因是：\" + error );\n                }\n            });\n        } else {\n            article.setReadStatus(App.STATUS_READED);\n            CoreDB.i().articleDao().update(article);\n            App.i().getApi().markArticleReaded(article.getId(), new CallbackX() {\n                @Override\n                public void onSuccess(Object result) {\n                }\n\n                @Override\n                public void onFailure(Object error) {\n                    article.setReadStatus(App.STATUS_UNREAD);\n                    CoreDB.i().articleDao().update(article);\n                }\n            });\n        }\n//        KLog.e(\"修改状态：\" + position + \"  \"  + article);\n        articlesAdapter.notifyItemChanged(position);\n    }\n\n\n    private void toggleStarState(final int position) {\n        if (position < 0) {\n            return;\n        }\n\n        Article article = articlesAdapter.getItem(position);\n//        String articleId = articlesAdapter.getItem(position).getId();\n//        Article article = CoreDB.i().articleDao().getById(App.i().getUser().getId(),articleId);\n\n        if (article.getStarStatus() == App.STATUS_STARED) {\n            article.setStarStatus(App.STATUS_UNSTAR);\n            CoreDB.i().articleDao().update(article);\n\n            App.i().getApi().markArticleUnstar(article.getId(), new CallbackX() {\n                 @Override\n                 public void onSuccess(Object result) {\n                 }\n                 @Override\n                 public void onFailure(Object error) {\n                     article.setStarStatus(App.STATUS_STARED);\n                     CoreDB.i().articleDao().update(article);\n                 }\n             });\n\n        } else {\n            article.setStarStatus(App.STATUS_STARED);\n            CoreDB.i().articleDao().update(article);\n            App.i().getApi().markArticleStared(article.getId(), new CallbackX() {\n                 @Override\n                 public void onSuccess(Object result) {\n                 }\n\n                 @Override\n                 public void onFailure(Object error) {\n                     article.setStarStatus(App.STATUS_UNSTAR);\n                     CoreDB.i().articleDao().update(article);\n                 }\n             });\n        }\n        articlesAdapter.notifyItemChanged(position);\n    }\n\n\n    // TODO: 2018/3/4 改用观察者模式。http://iaspen.cn/2015/05/09/观察者模式在android上的最佳实践\n    /**\n     * 在android中从A页面跳转到B页面，然后B页面进行某些操作后需要通知A页面去刷新数据，\n     * 我们可以通过startActivityForResult来唤起B页面，然后再B页面结束后在A页面重写onActivityResult来接收返回结果从而来刷新页面。\n     * 但是如果跳转路径是这样的A->B->C->…..，C或者C以后的页面来刷新A，这个时候如果还是使用这种方法就会非常的棘手。\n     * 使用这种方法可能会存在以下几个弊端：\n     * 1、多个路径或者多个事件的传递处理起来会非常困难。\n     * 2、数据更新不及时，往往需要用户去等待，降低系统性能和用户体验。\n     * 3、代码结构混乱，不易编码和扩展。\n     * 因此考虑使用观察者模式去处理这个问题。\n     */\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {\n        super.onActivityResult(requestCode, resultCode, intent);\n//        KLog.e(\"------------------------------------------\" + resultCode + requestCode);\n        switch (resultCode) {\n            case App.ActivityResult_ArtToMain:\n                // 在文章页的时候读到了第几篇文章，好让列表也自动将该项置顶\n                int articleNo = intent.getExtras().getInt(\"articleNo\");\n\n//                LinearLayoutManager linearLayoutManager = (LinearLayoutManager) articleListView.getLayoutManager();\n                assert linearLayoutManager != null;\n                if (articleNo > linearLayoutManager.findLastVisibleItemPosition() - 1) {\n                    slvSetSelection(articleNo);\n                }\n                articlesAdapter.notifyDataSetChanged();\n                break;\n            case App.ActivityResult_SearchLocalArtsToMain:\n//                KLog.e(\"被搜索的词是\" + intent.getExtras().getString(\"searchWord\"));\n                showSearchResult(intent.getExtras().getString(\"searchWord\"));\n                articlesAdapter.notifyDataSetChanged();\n                break;\n            default:\n                break;\n        }\n    }\n\n    // 滚动到指定位置\n    private void slvSetSelection(final int position) {\n        // 保证滚动到指定位置时，view至最顶端\n        LinearSmoothScroller smoothScroller = new LinearSmoothScroller(this) {\n            @Override\n            protected int getVerticalSnapPreference() {\n                return LinearSmoothScroller.SNAP_TO_START;\n            }\n        };\n        smoothScroller.setTargetPosition(position);\n        Objects.requireNonNull(articleListView.getLayoutManager()).startSmoothScroll(smoothScroller);\n    }\n\n    public void clickRefreshIcon(View view) {\n        refreshData();\n    }\n\n    public void onQuickSettingIconClicked(View view) {\n        quickSettingDialog = new BottomSheetDialog(MainActivity.this);\n        quickSettingDialog.setContentView(R.layout.main_bottom_sheet_more);\n//        quickSettingDialog.dismiss(); //dialog消失\n//        quickSettingDialog.setCanceledOnTouchOutside(false);  //触摸dialog之外的地方，dialog不消失\n//        quickSettingDialog.setCancelable(false); // dialog无法取消，按返回键都取消不了\n\n        View moreSetting = quickSettingDialog.findViewById(R.id.more_setting);\n        moreSetting.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View view) {\n                quickSettingDialog.dismiss();\n                Intent intent = new Intent(MainActivity.this, SettingActivity.class);\n                startActivity(intent);\n                overridePendingTransition(R.anim.in_from_bottom, R.anim.fade_out);\n            }\n        });\n\n        SwitchButton autoMarkWhenScrolling = quickSettingDialog.findViewById(R.id.auto_mark_when_scrolling_switch);\n        autoMarkWhenScrolling.setChecked(App.i().getUser().isMarkReadOnScroll());\n        autoMarkWhenScrolling.setOnCheckedChangeListener((compoundButton, b) -> {\n            KLog.e(\"onClickedAutoMarkWhenScrolling图标被点击\");\n            User user = App.i().getUser();\n            user.setMarkReadOnScroll(b);\n            CoreDB.i().userDao().update(user);\n            autoMarkReaded = b;\n            if (autoMarkReaded) {\n                vToolbarAutoMark.setVisibility(View.VISIBLE);\n            } else {\n                vToolbarAutoMark.setVisibility(View.GONE);\n            }\n        });\n\n        SwitchButton downImgOnWifiSwitch = quickSettingDialog.findViewById(R.id.down_img_on_wifi_switch);\n        downImgOnWifiSwitch.setChecked(App.i().getUser().isDownloadImgOnlyWifi());\n        downImgOnWifiSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {\n            @Override\n            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {\n                User user = App.i().getUser();\n                user.setDownloadImgOnlyWifi(b);\n                CoreDB.i().userDao().update(user);\n            }\n        });\n\n        SwitchButton nightThemeWifiSwitch = quickSettingDialog.findViewById(R.id.night_theme_switch);\n        nightThemeWifiSwitch.setChecked(App.i().getUser().getThemeMode() == App.THEME_NIGHT);\n        nightThemeWifiSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {\n            @Override\n            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {\n                quickSettingDialog.dismiss();\n                manualToggleTheme();\n            }\n        });\n\n        RadioGroup radioGroup = quickSettingDialog.findViewById(R.id.article_list_state_radio_group);\n        final RadioButton radioAll = quickSettingDialog.findViewById(R.id.radio_all);\n        final RadioButton radioUnread = quickSettingDialog.findViewById(R.id.radio_unread);\n        final RadioButton radioStarred = quickSettingDialog.findViewById(R.id.radio_starred);\n        if (App.i().getUser().getStreamStatus() == App.STATUS_STARED) {\n            radioStarred.setChecked(true);\n        } else if (App.i().getUser().getStreamStatus() == App.STATUS_UNREAD) {\n            radioUnread.setChecked(true);\n        } else {\n            radioAll.setChecked(true);\n        }\n        radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {\n            @Override\n            public void onCheckedChanged(RadioGroup radioGroup, int i) {\n                User user = App.i().getUser();\n                if (i == radioStarred.getId()) {\n                    user.setStreamStatus(App.STATUS_STARED);\n                    toolbar.setNavigationIcon(R.drawable.ic_state_star);\n                } else if (i == radioUnread.getId()) {\n                    user.setStreamStatus(App.STATUS_UNREAD);\n                    toolbar.setNavigationIcon(R.drawable.ic_state_unread);\n                } else {\n                    user.setStreamStatus(App.STATUS_ALL);\n                    toolbar.setNavigationIcon(R.drawable.ic_state_all);\n                }\n                CoreDB.i().userDao().update(user);\n                refreshData();\n                quickSettingDialog.dismiss();\n            }\n        });\n\n        IconFontView iconFontView = quickSettingDialog.findViewById(R.id.main_more_close);\n        iconFontView.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View view) {\n                quickSettingDialog.dismiss();\n            }\n        });\n\n        quickSettingDialog.show();\n    }\n\n\n    @OnClick(R.id.main_toolbar)\n    public void clickToolbar(View view) {\n        if (maHandler.hasMessages(App.MSG_DOUBLE_TAP)) {\n            maHandler.removeMessages(App.MSG_DOUBLE_TAP);\n            articleListView.smoothScrollToPosition(0);\n        } else {\n            maHandler.sendEmptyMessageDelayed(App.MSG_DOUBLE_TAP, ViewConfiguration.getDoubleTapTimeout());\n        }\n    }\n\n\n    /**\n     * 监听返回键，弹出提示退出对话框\n     */\n    @Override\n    public boolean onKeyDown(int keyCode, KeyEvent event) {\n        // 后者为短期内按下的次数\n        if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {\n            quitDialog();\n            //返回真表示返回键被屏蔽掉\n            return true;\n        }\n        return super.onKeyDown(keyCode, event);\n    }\n\n    private void quitDialog() {\n        new AlertDialog.Builder(this)\n                .setMessage(R.string.main_dialog_esc_confirm)\n                .setPositiveButton(R.string.main_dialog_esc_positive, new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialog, int which) {\n                        System.exit(0);\n                    }\n                })\n                .setNegativeButton(R.string.main_dialog_esc_negative, new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialog, int which) {\n                        dialog.dismiss();\n                    }\n                })\n                .show();\n    }\n\n    private RelativeLayout bottomBar;\n    private void initToolbar() {\n        toolbar = findViewById(R.id.main_toolbar);\n        setSupportActionBar(toolbar);\n        // 这个小于4.0版本是默认为true，在4.0及其以上是false。该方法的作用：决定左上角的图标是否可以点击(没有向左的小图标)，true 可点\n        getSupportActionBar().setHomeButtonEnabled(true);\n        // 决定左上角图标的左侧是否有向左的小箭头，true 有小箭头\n        getSupportActionBar().setDisplayHomeAsUpEnabled(false);\n        getSupportActionBar().setDisplayShowTitleEnabled(true);\n\n        if (App.i().getUser().getStreamStatus() == App.STATUS_ALL) {\n            toolbar.setNavigationIcon(R.drawable.ic_state_all);\n        } else if (App.i().getUser().getStreamStatus() == App.STATUS_STARED) {\n            toolbar.setNavigationIcon(R.drawable.ic_state_star);\n        } else {\n            toolbar.setNavigationIcon(R.drawable.ic_state_unread);\n        }\n        // 左上角图标是否显示，false则没有程序图标，仅标题。否则显示应用程序图标，对应id为android.R.id.home，对应ActionBar.DISPLAY_SHOW_HOME\n        // setDisplayShowHomeEnabled(true)\n        // 使自定义的普通View能在title栏显示，即actionBar.setCustomView能起作用，对应ActionBar.DISPLAY_SHOW_CUSTOM\n        // setDisplayShowCustomEnabled(true)\n\n        bottomBar = findViewById(R.id.main_bottombar);\n    }\n\n\n    /**\n     * 设置各个视图与颜色属性的关联\n     */\n    @Override\n    protected Colorful.Builder buildColorful(Colorful.Builder mColorfulBuilder) {\n        ViewGroupSetter articlesHeaderVS = new ViewGroupSetter((ViewGroup) articlesHeaderView);\n        articlesHeaderVS.childViewBgColor(R.id.main_header, R.attr.root_view_bg);\n        articlesHeaderVS.childViewTextColor(R.id.main_header_title, R.attr.lv_item_desc_color);\n        // articlesHeaderVS.childViewBgDrawable(R.id.main_header_eye, R.attr.lv_item_desc_color);\n\n        ViewGroupSetter artListViewSetter = new ViewGroupSetter(articleListView);\n        // 绑定ListView的Item View中的news_title视图，在换肤时修改它的text_color属性\n        // artListViewSetter.childViewBgColor(R.id.main_slv_item, R.attr.root_view_bg);\n        artListViewSetter.childViewTextColor(R.id.main_slv_item_title, R.attr.lv_item_title_color);\n        artListViewSetter.childViewTextColor(R.id.main_slv_item_summary, R.attr.lv_item_desc_color);\n        artListViewSetter.childViewTextColor(R.id.main_slv_item_author, R.attr.lv_item_info_color);\n        artListViewSetter.childViewTextColor(R.id.main_slv_item_time, R.attr.lv_item_info_color);\n        artListViewSetter.childViewBgColor(R.id.main_slv_item_divider, R.attr.lv_item_divider);\n        artListViewSetter.childViewBgColor(R.id.main_list_item_surface, R.attr.root_view_bg);\n        // artListViewSetter.childViewBgColor(R.id.main_list_item_menu_left, R.attr.root_view_bg);\n        // artListViewSetter.childViewBgColor(R.id.main_list_item_menu_right, R.attr.root_view_bg);\n        // artListViewSetter.childViewBgColor(R.id.swipe_layout, R.attr.root_view_bg);\n\n        // 绑定ListView的Item View中的news_title视图，在换肤时修改它的text_color属性\n        ViewGroupSetter tagListViewSetter = new ViewGroupSetter(tagListView);\n        tagListViewSetter.childViewBgColor(R.id.group_item, R.attr.root_view_bg);  // 这个不生效，反而会影响底色修改\n        tagListViewSetter.childViewTextColor(R.id.group_item_icon, R.attr.tag_slv_item_icon);\n        tagListViewSetter.childViewTextColor(R.id.group_item_title, R.attr.lv_item_title_color);\n        tagListViewSetter.childViewTextColor(R.id.group_item_count, R.attr.lv_item_desc_color);\n        // tagListViewSetter.childViewBgDrawable(R.id.group_item_count, R.attr.bubble_bg);\n\n        ViewGroupSetter relative = new ViewGroupSetter(relativeLayout);\n        relative.childViewBgColor(R.id.main_tag_close, R.attr.root_view_bg);\n        relative.childViewTextColor(R.id.main_tag_close, R.attr.bottombar_fg);\n        relative.childViewBgColor(R.id.sheet_tag, R.attr.root_view_bg);\n        relative.childViewBgColor(R.id.main_tag_list_view, R.attr.root_view_bg);\n\n\n        tagListViewSetter.childViewBgColor(R.id.child_item, R.attr.root_view_bg);  // 这个不生效，反而会影响底色修改\n        tagListViewSetter.childViewTextColor(R.id.child_item_title, R.attr.lv_item_title_color);\n        tagListViewSetter.childViewTextColor(R.id.child_item_count, R.attr.lv_item_desc_color);\n//        tagListViewSetter.childViewBgDrawable(R.id.child_item_count, R.attr.bubble_bg);\n\n//        ViewGroupSetter headerHomeViewSetter = new ViewGroupSetter((ViewGroup) headerHomeView);\n//        headerHomeViewSetter.childViewBgColor(R.id.header_home, R.attr.root_view_bg);  // 这个不生效，反而会影响底色修改\n//        headerHomeViewSetter.childViewTextColor(R.id.header_home_icon, R.attr.tag_slv_item_icon);\n//        headerHomeViewSetter.childViewTextColor(R.id.header_home_title, R.attr.lv_item_title_color);\n//        headerHomeViewSetter.childViewTextColor(R.id.header_home_count, R.attr.lv_item_desc_color);\n\n//        ViewGroupSetter headerPinnedViewSetter = new ViewGroupSetter((ViewGroup) headerPinnedView);\n//        headerPinnedViewSetter.childViewBgColor(R.id.header_item, R.attr.root_view_bg);\n//        headerPinnedViewSetter.childViewTextColor(R.id.header_item_icon, R.attr.tag_slv_item_icon);\n//        headerPinnedViewSetter.childViewTextColor(R.id.header_item_title, R.attr.lv_item_title_color);\n//        headerPinnedViewSetter.childViewTextColor(R.id.header_item_count, R.attr.lv_item_desc_color);\n//        headerPinnedViewSetter.childViewBgDrawable(R.id.header_item_count, R.attr.bubble_bg);\n\n        mColorfulBuilder\n                // 这里做设置，实质都是直接生成了一个View（根据Activity的findViewById），并直接添加到 colorful 内的 mElements 中。\n                .backgroundColor(R.id.main_swipe_refresh, R.attr.root_view_bg)\n                // 设置 toolbar\n                .backgroundColor(R.id.main_toolbar, R.attr.topbar_bg)\n                //.textColor(R.id.main_toolbar_hint, R.attr.topbar_fg)\n\n                .backgroundColor(R.id.sheet_tag, R.attr.root_view_bg)\n\n                // 设置 bottombar\n                .backgroundColor(R.id.main_bottombar, R.attr.bottombar_bg)\n                // 设置中屏和底栏之间的分割线\n                .backgroundColor(R.id.main_bottombar_divider, R.attr.bottombar_divider)\n                .textColor(R.id.main_bottombar_search, R.attr.bottombar_fg)\n                .textColor(R.id.main_bottombar_setting, R.attr.bottombar_fg)\n                .textColor(R.id.main_bottombar_tag, R.attr.bottombar_fg)\n                .textColor(R.id.main_bottombar_refresh_articles, R.attr.bottombar_fg)\n\n                // 设置 listview 背景色\n                // 这里做设置，实质是将View（根据Activity的findViewById），并直接添加到 colorful 内的 mElements 中。\n                .setter(relative)\n//                .setter(headerPinnedViewSetter)\n//                .setter(headerHomeViewSetter)\n                .setter(articlesHeaderVS)\n                .setter(artListViewSetter)\n                .setter(tagListViewSetter);\n        return mColorfulBuilder;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/MusicActivity.java",
    "content": "package me.wizos.loread.activity;\n\nimport android.content.ComponentName;\nimport android.content.Intent;\nimport android.content.ServiceConnection;\nimport android.graphics.drawable.Drawable;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.IBinder;\nimport android.text.TextUtils;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.view.animation.BounceInterpolator;\nimport android.widget.ImageView;\nimport android.widget.SeekBar;\nimport android.widget.TextView;\n\nimport androidx.appcompat.widget.Toolbar;\n\nimport com.freedom.lauzy.playpauseviewlib.PlayPauseView;\nimport com.hjq.permissions.OnPermission;\nimport com.hjq.permissions.Permission;\nimport com.hjq.permissions.XXPermissions;\nimport com.hjq.toast.ToastUtils;\nimport com.lxj.xpopup.XPopup;\nimport com.lxj.xpopup.enums.PopupAnimation;\nimport com.lxj.xpopup.interfaces.OnSelectListener;\nimport com.noober.background.drawable.DrawableCreator;\nimport com.socks.library.KLog;\nimport com.yhao.floatwindow.constant.MoveType;\nimport com.yhao.floatwindow.constant.Screen;\nimport com.yhao.floatwindow.view.FloatWindow;\n\nimport java.util.List;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.User;\nimport me.wizos.loread.service.MusicService;\nimport me.wizos.loread.utils.ScreenUtil;\nimport me.wizos.loread.utils.TimeUtil;\nimport me.wizos.loread.view.colorful.Colorful;\n\npublic class MusicActivity extends BaseActivity {\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_music);\n        initToolbar();\n\n        Intent intent = getIntent();\n        String playUrl = intent.getDataString();\n\n        // 补救，获取 playUrl\n        if (TextUtils.isEmpty(playUrl)) {\n            playUrl = intent.getStringExtra(Intent.EXTRA_TEXT);\n        }\n        String title = intent.getStringExtra(\"title\");\n\n        KLog.e(\"获取到链接：\" + title + playUrl);\n\n        playConnection = new PlayConnection();\n        intent = new Intent(this, MusicService.class);\n\n        if (!TextUtils.isEmpty(playUrl)) {\n            intent.setData(Uri.parse(playUrl));\n            intent.putExtra(\"title\", title);\n            initFloatWindow();\n        }\n        startService(intent);\n        bindService(intent, playConnection, BIND_AUTO_CREATE);\n        applyPermissions();\n    }\n\n\n    private void applyPermissions() {\n        XXPermissions.with(this)\n                //.constantRequest() //可设置被拒绝后继续申请，直到用户授权或者永久拒绝\n                .permission(Permission.SYSTEM_ALERT_WINDOW) //支持请求6.0悬浮窗权限8.0请求安装权限\n                .request(new OnPermission() {\n                    @Override\n                    public void hasPermission(List<String> granted, boolean isAll) {\n                    }\n\n                    @Override\n                    public void noPermission(List<String> denied, boolean quick) {\n                        for (String id : denied) {\n                            KLog.e(\"无法获取权限\" + id);\n                        }\n                        ToastUtils.show(getString(R.string.plz_grant_permission_tips));\n                    }\n                });\n    }\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        maHandler.removeCallbacksAndMessages(null);\n        if (playConnection != null) {\n            //退出应用后与service解除绑定\n            unbindService(playConnection);\n        }\n    }\n\n    @Override\n    protected Colorful.Builder buildColorful(Colorful.Builder mColorfulBuilder) {\n        return mColorfulBuilder;\n    }\n\n    private boolean isChangeProgress = false;\n    protected SeekBar seekBar;\n    protected TextView speedView;\n    protected TextView currTimeView;\n    protected TextView totalTimeView;\n    protected PlayPauseView playPauseView;\n    protected PlayConnection playConnection;\n    protected MusicService.MusicControlBinder musicControl;\n\n    protected static Handler maHandler = new Handler();\n\n    protected Runnable progressTask = new Runnable() {\n        @Override\n        public void run() {\n            int currentPosition = musicControl.getCurrentPosition();\n            int duration = musicControl.getDuration();\n\n            if (seekBar != null && !isChangeProgress) {\n                seekBar.setProgress(currentPosition);\n                seekBar.setSecondaryProgress(musicControl.getBufferedPercent() * duration / 100);\n                seekBar.setMax(duration);\n            }\n            if (currTimeView != null && !isChangeProgress) {\n                currTimeView.setText(TimeUtil.getTime(currentPosition));\n                totalTimeView.setText(TimeUtil.getTime(duration));\n            }\n            //KLog.e(\"进度：\" + seekBar + \", \" + currTimeView + \" , \" + duration + \" = \"  + TimeUtil.getTime(duration));\n            maHandler.postDelayed(progressTask, 1000);\n        }\n    };\n\n    public class PlayConnection implements ServiceConnection {\n        //服务启动完成后会进入到这个方法\n        @Override\n        public void onServiceConnected(ComponentName name, IBinder service) {\n            //获得service中的MyBinder\n            KLog.e(\"服务连接：onServiceConnected\" + musicControl);\n            initView(service);\n        }\n\n        @Override\n        public void onServiceDisconnected(ComponentName name) {\n        }\n    }\n\n    public void initToolbar() {\n        Toolbar toolbar = findViewById(R.id.music_toolbar);\n        setSupportActionBar(toolbar);\n        // 这个小于4.0版本是默认为true，在4.0及其以上是false。该方法的作用：决定左上角的图标是否可以点击(没有向左的小图标)，true 可点\n        getSupportActionBar().setHomeButtonEnabled(true);\n        // 决定左上角图标的左侧是否有向左的小箭头，true 有小箭头\n        getSupportActionBar().setDisplayHomeAsUpEnabled(true);\n        getSupportActionBar().setDisplayShowTitleEnabled(false);\n        getSupportActionBar().setTitle(getString(R.string.music));\n        toolbar.setTitle(getString(R.string.music));\n        toolbar.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                MusicActivity.this.finish();\n            }\n        });\n    }\n\n    public void initView(IBinder service) {\n        musicControl = (MusicService.MusicControlBinder) service;\n        ImageView closeView = findViewById(R.id.music_close);\n        TextView titleView = findViewById(R.id.music_title);\n        playPauseView = findViewById(R.id.music_play_pause_view);\n        currTimeView = findViewById(R.id.currTime);\n        totalTimeView = findViewById(R.id.totalTime);\n        seekBar = findViewById(R.id.progressBar);\n        speedView = findViewById(R.id.music_speed);\n\n        titleView.setText(musicControl.getTitle());\n\n        if (musicControl.isPlaying()) {\n            playPauseView.play();\n            maHandler.post(progressTask);\n        } else {\n            playPauseView.pause();\n            currTimeView.setText(TimeUtil.getTime(musicControl.getCurrentPosition()));\n            totalTimeView.setText(TimeUtil.getTime(musicControl.getDuration()));\n            seekBar.setProgress(musicControl.getCurrentPosition());\n        }\n\n\n        musicControl.setPlayStatusListener(new MusicService.PlayStatusListener() {\n            @Override\n            public void onPlay() {\n                playPauseView.play();\n                musicControl.setSpeed(App.i().getUser().getAudioSpeed());\n                maHandler.postDelayed(progressTask, 1000);\n            }\n\n            @Override\n            public void onPause() {\n                playPauseView.pause();\n                maHandler.removeCallbacks(progressTask);\n            }\n\n            @Override\n            public void onEnd() {\n                playPauseView.pause();\n                maHandler.removeCallbacks(progressTask);\n                MusicActivity.this.finish();\n            }\n\n            @Override\n            public void onError(String cause) {\n                ToastUtils.show(\"系统出错：\" + cause);\n                playPauseView.pause();\n                maHandler.removeCallbacks(progressTask);\n            }\n        });\n        playPauseView.setPlayPauseListener(new PlayPauseView.PlayPauseListener() {\n            @Override\n            public void play() {\n                musicControl.play();\n                maHandler.removeCallbacks(progressTask);\n                maHandler.postDelayed(progressTask, 1000);\n            }\n\n            @Override\n            public void pause() {\n                maHandler.removeCallbacks(progressTask);\n                musicControl.pause();\n            }\n        });\n\n        seekBar.setMax(musicControl.getDuration());\n        seekBar.setProgress(musicControl.getCurrentPosition());\n        seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {\n            @Override\n            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {\n                if (fromUser && currTimeView != null) {\n                    currTimeView.setText(TimeUtil.getTime(progress));\n                }\n            }\n\n            //开始触摸进度条，停止更新进度条\n            @Override\n            public void onStartTrackingTouch(SeekBar seekBar) {\n                isChangeProgress = true;\n            }\n\n            //停止触摸进度条\n            @Override\n            public void onStopTrackingTouch(SeekBar seekBar) {\n                isChangeProgress = false;\n                musicControl.seekTo(seekBar.getProgress());\n            }\n        });\n\n\n        closeView.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                maHandler.removeCallbacks(progressTask);\n                // 关闭悬浮窗\n                FloatWindow.destroy();\n                // 关闭 serview\n                Intent intent2 = new Intent(MusicActivity.this, MusicService.class);\n                stopService(intent2);\n                // 关闭 activity\n                MusicActivity.this.finish();\n            }\n        });\n\n\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {\n            speedView.setVisibility(View.GONE);\n        } else {\n            speedView.setText(App.i().getUser().getAudioSpeed() + \"\");\n            speedView.setOnClickListener(new View.OnClickListener() {\n                @Override\n                public void onClick(View v) {\n                    new XPopup.Builder(MusicActivity.this)\n                            .isCenterHorizontal(true) //是否与目标水平居中对齐\n                            .offsetY(-10)\n                            .hasShadowBg(true)\n                            .popupAnimation(PopupAnimation.ScaleAlphaFromCenter)\n                            .atView(speedView)  // 依附于所点击的View，内部会自动判断在上方或者下方显示\n                            .asAttachList(new String[]{\"0.8\", \"1.0\", \"1.2\", \"1.5\", \"2.0\"},\n                                    null,\n                                    new OnSelectListener() {\n                                        @Override\n                                        public void onSelect(int which, String text) {\n                                            musicControl.setSpeed(Float.parseFloat(text));\n                                            User user = App.i().getUser();\n                                            user.setAudioSpeed(Float.parseFloat(text));\n                                            //App.i().getUserBox().put(user);\n                                            CoreDB.i().userDao().update(user);\n                                            speedView.setText(text);\n                                        }\n                                    })\n                            .show();\n                }\n            });\n        }\n    }\n\n\n    private void initFloatWindow() {\n        ImageView imageView = new ImageView(this);\n        imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);\n        imageView.setPadding(ScreenUtil.dp2px(10), ScreenUtil.dp2px(10), ScreenUtil.dp2px(10), ScreenUtil.dp2px(10));\n        imageView.setImageDrawable(getDrawable(R.drawable.ic_music));\n\n        //imageView.setBackground(getDrawable(R.drawable.shape_corners));\n        Drawable drawable = new DrawableCreator.Builder()\n//                .setUnPressedDrawable( getDrawable(R.color.bluePrimary) )\n                .setRipple(true, getResources().getColor(R.color.primary))\n                .setPressedSolidColor(getResources().getColor(R.color.primary), getResources().getColor(R.color.bluePrimary))\n                .setSolidColor(getResources().getColor(R.color.bluePrimary))\n                .setCornersRadius(ScreenUtil.dp2px(30))\n                .build();\n        imageView.setBackground(drawable);\n        imageView.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                Intent intent = new Intent(getApplicationContext(), MusicActivity.class);\n                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n                startActivity(intent);\n            }\n        });\n\n\n        FloatWindow\n                .with(getApplicationContext())\n                .setView(imageView)\n                .setWidth(Screen.width, 0.15f) //设置悬浮控件宽高\n                .setHeight(Screen.width, 0.15f)\n                .setX(Screen.width, 0.8f) //设置控件初始位置\n                .setY(Screen.height, 0.8f)\n                .setMoveType(MoveType.slide, 10, 10, 10, 10)\n                .setMoveStyle(500, new BounceInterpolator())\n                .setFilter(true, MainActivity.class, ArticleActivity.class)\n                .setDesktopShow(false)\n                .build();\n    }\n\n\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        if( item.getItemId() == android.R.id.home ){\n            this.finish();\n            overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/ProviderActivity.java",
    "content": "package me.wizos.loread.activity;\n\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.text.TextUtils;\nimport android.view.View;\n\nimport com.hjq.toast.ToastUtils;\nimport com.lxj.xpopup.XPopup;\nimport com.lxj.xpopup.impl.LoadingPopupView;\nimport com.socks.library.KLog;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.Contract;\nimport me.wizos.loread.R;\nimport me.wizos.loread.activity.login.LoginInoReaderActivity;\nimport me.wizos.loread.activity.login.LoginTinyRSSActivity;\nimport me.wizos.loread.bean.Token;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.User;\nimport me.wizos.loread.network.api.FeedlyApi;\nimport me.wizos.loread.network.api.InoReaderApi;\nimport me.wizos.loread.network.api.OAuthApi;\nimport me.wizos.loread.network.callback.CallbackX;\nimport me.wizos.loread.view.colorful.Colorful;\n\npublic class ProviderActivity extends BaseActivity {\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        if(Build.VERSION.SDK_INT < 23){\n            setContentView(R.layout.activity_provider_low_version);\n        }else {\n            setContentView(R.layout.activity_provider);\n        }\n    }\n\n    public void loginInoReader(View view){\n        Intent intent = new Intent(this, LoginInoReaderActivity.class);\n        startActivityForResult(intent, App.ActivityResult_LoginPageToProvider);\n        overridePendingTransition(R.anim.fade_in, R.anim.fade_out);\n    }\n    public void oauthInoReader(View view) {\n        Intent intent = new Intent(this, WebActivity.class);\n        intent.setData(Uri.parse(new InoReaderApi().getOAuthUrl()));\n        startActivity(intent);\n        overridePendingTransition(R.anim.fade_in, R.anim.fade_out);\n    }\n    public void oauthFeedly(View view) {\n        Intent intent = new Intent(this, WebActivity.class);\n        intent.setData(Uri.parse(new FeedlyApi().getOAuthUrl()));\n        startActivity(intent);\n        overridePendingTransition(R.anim.fade_in, R.anim.fade_out);\n    }\n    public void loginTinyRSS(View view) {\n        Intent intent = new Intent(this, LoginTinyRSSActivity.class);\n        startActivityForResult(intent, App.ActivityResult_LoginPageToProvider);\n        overridePendingTransition(R.anim.fade_in, R.anim.fade_out);\n    }\n\n//    // TODO: 2019/2/16  跳转到RSS添加的发现页\n//    public void selectLocalRSS(View view) {\n//        String uid = Contract.PROVIDER_LOCALRSS + \"_\" + getString(R.string.app_id);\n//        User user = CoreDB.i().userDao().getById(uid);\n//        if ( user == null ){\n//            user = new User();\n//            user.setId(uid);\n//            user.setSource(Contract.PROVIDER_LOCALRSS);\n//            user.setUserId(getString(R.string.app_id));\n//            user.setUserName(getString(R.string.app_name));\n//            user.setExpiresTimestamp(0);\n//            CoreDB.i().userDao().insert(user);\n//        }\n//\n//        App.i().getGlobalKV().putString(Contract.UID, uid);\n//        route();\n//    }\n\n\n    private void getAccessToken(final String code, OAuthApi api) {\n        final LoadingPopupView dialog = new XPopup.Builder(this)\n                .dismissOnTouchOutside(false)\n                .asLoading(getString(R.string.authing));\n        dialog.show();\n\n        api.getAccessToken(code, new CallbackX<Token,String>() {\n            @Override\n            public void onSuccess(Token token) {\n                KLog.e(\"授权为：\" + token);\n                dialog.setTitle(getString(R.string.fetch_user_info));\n                api.setAuthorization(token.getAuth());\n                api.fetchUserInfo(new CallbackX<User,String>() {\n                    @Override\n                    public void onSuccess(User user) {\n                        KLog.e(\"用户资料：\" + user + token.getAuth());\n                        user.setToken(token);\n\n                        App.i().getKeyValue().putString(Contract.UID, user.getId());\n                        App.i().setApi(api);\n                        CoreDB.i().userDao().insert(user);\n                        dialog.dismiss();\n                        KLog.e(token);\n                        App.i().restartApp();\n                    }\n\n                    @Override\n                    public void onFailure(String error) {\n                        ToastUtils.show(getString(R.string.login_failure_please_try_again) + error);\n                        dialog.dismiss();\n                    }\n                });\n            }\n\n            @Override\n            public void onFailure(String error) {\n            }\n        });\n    }\n\n\n    @Override\n    protected void onNewIntent(Intent paramIntent) {\n        super.onNewIntent(paramIntent);\n        String url = paramIntent.getDataString();\n        KLog.e(\"获取到数据：\" + url);\n        if (TextUtils.isEmpty(url)) {\n            ToastUtils.show(getString(R.string.auth_failure_please_try_again));\n            return;\n        }\n        Uri uri = Uri.parse(url);\n        String host = uri.getHost().toLowerCase();\n        String code = uri.getQueryParameter(\"code\");\n        if (TextUtils.isEmpty(host)) {\n            ToastUtils.show(getString(R.string.auth_failure_please_try_again));\n            return;\n        }\n\n        if (TextUtils.isEmpty(code)) {\n            ToastUtils.show(\"无法获取code\");\n            return;\n        }\n\n        if (host.contains(\"feedlyauth\")) {\n            getAccessToken(code, new FeedlyApi());\n        } else if (host.contains(Contract.PROVIDER_INOREADER.toLowerCase())) {\n            getAccessToken(code, new InoReaderApi());\n        }\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {\n        super.onActivityResult(requestCode, resultCode, intent);\n        KLog.e(\"---------\" + resultCode + requestCode);\n        if (resultCode == App.ActivityResult_LoginPageToProvider) {\n            App.i().getUser();\n            App.i().restartApp();\n        }\n    }\n\n    public Colorful.Builder buildColorful(Colorful.Builder mColorfulBuilder) {\n        return mColorfulBuilder;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/RuleGenerateActivity.java",
    "content": "package me.wizos.loread.activity;\n\nimport android.os.Bundle;\n\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport me.wizos.loread.R;\n\npublic class RuleGenerateActivity extends AppCompatActivity {\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_rule_generate);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/SearchActivity.java",
    "content": "package me.wizos.loread.activity;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.text.Editable;\nimport android.text.TextUtils;\nimport android.text.TextWatcher;\nimport android.view.KeyEvent;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.inputmethod.InputMethodManager;\nimport android.widget.ArrayAdapter;\nimport android.widget.EditText;\nimport android.widget.ImageView;\nimport android.widget.ListView;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.widget.Toolbar;\n\nimport com.afollestad.materialdialogs.DialogAction;\nimport com.afollestad.materialdialogs.MaterialDialog;\nimport com.bumptech.glide.Glide;\nimport com.bumptech.glide.request.RequestOptions;\nimport com.hjq.toast.ToastUtils;\nimport com.socks.library.KLog;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.bean.feedly.CategoryItem;\nimport me.wizos.loread.bean.feedly.input.EditFeed;\nimport me.wizos.loread.bean.search.SearchFeedItem;\nimport me.wizos.loread.db.Category;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.network.callback.CallbackX;\nimport me.wizos.loread.utils.TimeUtil;\nimport me.wizos.loread.view.IconFontView;\nimport me.wizos.loread.view.SwipeRefreshLayoutS;\nimport me.wizos.loread.view.colorful.Colorful;\nimport me.wizos.loread.view.colorful.setter.ViewGroupSetter;\n\n\npublic class SearchActivity extends BaseActivity {\n    protected static final String TAG = \"SearchActivity\";\n    private EditText searchView;\n    private ListView listView;\n    private SwipeRefreshLayoutS swipeRefreshLayoutS;\n\n    private ArrayList<SearchFeedItem> searchFeedItems = new ArrayList<>();\n    private SearchListViewAdapter listViewAdapter;\n    private View wordHeaderView, resultCountHeaderView;\n    private RequestOptions options;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_search);\n        initToolbar();\n        initView();\n        options = new RequestOptions()\n                .placeholder(R.mipmap.ic_launcher)\n                //.circleCrop()\n                .centerCrop();\n    }\n\n\n    private void initView() {\n        swipeRefreshLayoutS = findViewById(R.id.search_swipe_refresh);\n        searchView = findViewById(R.id.search_toolbar_edittext);\n        listView = findViewById(R.id.search_list_view);\n\n        swipeRefreshLayoutS.setEnabled(false);\n        // headerView\n        // 热门搜索，最近搜索\n        // 本地源、本地文章\n        // 云端源、云端文章\n        // 订阅\n        // 特定站点：微博，微信，知乎，BiliBili，Ins，G+，Facebook，关键词订阅，\n\n        wordHeaderView = getLayoutInflater().inflate(R.layout.activity_search_list_header_word, listView, false);\n        resultCountHeaderView = getLayoutInflater().inflate(R.layout.activity_search_list_header_result_count, listView, false);\n\n        listViewAdapter = new SearchListViewAdapter(SearchActivity.this, searchFeedItems);\n        listView.setAdapter(listViewAdapter);\n\n        searchView.requestFocus();\n        searchView.addTextChangedListener(new TextWatcher() {\n            @Override\n            public void beforeTextChanged(CharSequence s, int start, int count, int after) {\n//                KLog.e(\"输入前确认执行该方法\", \"开始输入：\" + s .toString() + startAnimation + \"  \" + after + \"  \" + count);\n            }\n\n            @Override\n            public void onTextChanged(CharSequence s, int start, int before, int count) {\n                if (s.toString().equals(\"\")) {\n                    listView.removeHeaderView(wordHeaderView);\n                    listView.removeHeaderView(resultCountHeaderView);\n                    swipeRefreshLayoutS.setRefreshing(false);\n                    searchFeedItems.clear();\n                } else if (listView.getHeaderViewsCount() == 0) {\n                    KLog.e(\"直接变成搜索该关键词\");\n                    listView.addHeaderView(wordHeaderView);\n                }\n            }\n\n            @Override\n            public void afterTextChanged(Editable s) {\n//                KLog.e(\"输入结束执行该方法\", \"输入结束\");\n            }\n        });\n\n        searchView.setOnKeyListener(new View.OnKeyListener() {\n            @Override\n            public boolean onKey(View v, int keyCode, KeyEvent event) {\n                if (keyCode == KeyEvent.KEYCODE_ENTER) {\n                    onSearchFeedsClicked(null);\n                }\n                return false;\n            }\n        });\n\n//        listView.setOnScrollListener(new AbsListView.OnScrollListener() {\n//            @Override\n//            public void onScrollStateChanged(AbsListView view, int scrollState) {\n//                if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {\n////                    if (view.getLastVisiblePosition() == view.getCount() - 1) {\n////                    }\n//                }\n//            }\n//            @Override\n//            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {\n//            }\n//        });\n    }\n\n    private void searchAndLoadFeedsData() {\n        swipeRefreshLayoutS.setRefreshing(true);\n        listView.setEnabled(false);\n        listView.removeHeaderView(wordHeaderView);\n        listView.removeHeaderView(resultCountHeaderView);\n\n//        Retrofit retrofit = new Retrofit.Builder()\n//                .baseUrl(FeedlyApi.HOST + \"/\") // 设置网络请求的Url地址, 必须以/结尾\n//                .addConverterFactory(GsonConverterFactory.create())  // 设置数据解析器\n//                .client(HttpClientManager.i().simpleClient())\n//                .build();\n//        FeedlyService feedlyService = retrofit.create(FeedlyService.class);\n//\n//        //对 发送请求 进行封装\n//        Call<SearchFeeds> callSearchFeeds = feedlyService.getSearchFeeds(searchView.getText().toString(), 100);\n//        callSearchFeeds.enqueue(new Callback<SearchFeeds>() {\n//            @Override\n//            public void onResponse(Call<SearchFeeds> call, retrofit2.Response<SearchFeeds> response) {\n//                SearchFeeds searchResult = response.body();\n//                KLog.e(\"成功：\" + searchResult);\n//                if (searchResult != null && searchResult.getResults() != null && searchResult.getResults().size() != 0) {\n//                    searchFeedItems = searchResult.getResults();\n//                    //  KLog.e(\"点击搜索\" + searchView.getText().toString() + searchFeedItems.size());\n//                    //  ToastUtil.show(\"已获取到\" + searchFeedItems.size() + \"个订阅源\");\n//                } else {\n//                    searchFeedItems = new ArrayList<SearchFeedItem>();\n//                }\n//                listViewAdapter = new SearchListViewAdapter(SearchActivity.this, searchFeedItems);\n//                TextView textView = resultCountHeaderView.findViewById(R.id.search_feeds_result_count);\n//                textView.setText(getString(R.string.search_cloudy_feeds_result_count, searchFeedItems.size()));\n//                listView.addHeaderView(resultCountHeaderView);\n//                listView.setAdapter(listViewAdapter);\n//                swipeRefreshLayoutS.setRefreshing(false);\n//                listView.setEnabled(true);\n//            }\n//\n//            @Override\n//            public void onFailure(Call<SearchFeeds> call, Throwable t) {\n//                KLog.e(\"失败：\" + t);\n//                ToastUtils.show(App.i().getString(R.string.fail_try));\n//                swipeRefreshLayoutS.setRefreshing(false);\n//                listView.setEnabled(true);\n//                listView.addHeaderView(wordHeaderView);\n//            }\n//        });\n    }\n\n    private Integer[] selectIndices;\n\n    public void showSelectFolder(final View view, final String feedId) {\n        final List<Category> categoryList = CoreDB.i().categoryDao().getAll(App.i().getUser().getId());\n        String[] categoryTitleArray = new String[categoryList.size()];\n        for (int i = 0, size = categoryList.size(); i < size; i++) {\n            categoryTitleArray[i] = categoryList.get(i).getTitle();\n        }\n        final EditFeed editFeed = new EditFeed();\n        editFeed.setId(feedId);\n        new MaterialDialog.Builder(this)\n                .title(getString(R.string.select_category))\n                .items(categoryTitleArray)\n                .alwaysCallMultiChoiceCallback()\n                .itemsCallbackMultiChoice(null, new MaterialDialog.ListCallbackMultiChoice() {\n                    @Override\n                    public boolean onSelection(MaterialDialog dialog, Integer[] which, CharSequence[] text) {\n                        SearchActivity.this.selectIndices = which;\n                        for (int i : which) {\n                            KLog.e(\"点选了：\" + i);\n                        }\n                        return true;\n                    }\n                })\n                .positiveText(R.string.confirm)\n                .onPositive(new MaterialDialog.SingleButtonCallback() {\n                    @Override\n                    public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n                        ArrayList<CategoryItem> categoryItemList = new ArrayList<>();\n                        for (int i = 0; i < selectIndices.length; i++) {\n                            CategoryItem categoryItem = new CategoryItem();\n                            categoryItem.setId(categoryList.get(selectIndices[i]).getId());\n                            categoryItemList.add(categoryItem);\n                        }\n                        editFeed.setCategoryItems(categoryItemList);\n                        view.setClickable(false);\n                        App.i().getApi().addFeed(editFeed, new CallbackX() {\n                            @Override\n                            public void onSuccess(Object result) {\n                                KLog.e(\"添加成功\");\n                                ((IconFontView) view).setText(R.string.font_tick);\n                                ToastUtils.show(R.string.subscribe_success);\n                                view.setClickable(true);\n                            }\n\n                            @Override\n                            public void onFailure(Object error) {\n                                ToastUtils.show(getString(R.string.subscribe_fail));\n                                view.setClickable(true);\n                            }\n                        });\n//                        App.i().getApi().addFeed(editFeed).enqueue(new Callback() {\n//                            @Override\n//                            public void onResponse(Call call, retrofit2.Response response) {\n//                                KLog.e(\"添加成功\");\n//                                ((IconFontView) view).setText(R.string.font_tick);\n//                                ToastUtils.show(R.string.subscribe_success);\n//                                view.setClickable(true);\n//                            }\n//\n//                            @Override\n//                            public void onFailure(Call call, Throwable t) {\n//                                ToastUtils.show(getString(R.string.subscribe_fail));\n//                                view.setClickable(true);\n//                            }\n//                        });\n                    }\n                }).show();\n    }\n\n    class SearchListViewAdapter extends ArrayAdapter<SearchFeedItem> {\n        private List<SearchFeedItem> searchFeedItems;\n\n        public SearchListViewAdapter(Context context, List<SearchFeedItem> feedList) {\n            super(context, 0, feedList);\n            this.searchFeedItems = feedList;\n        }\n\n        @Override\n        public int getCount() {\n            return searchFeedItems.size();\n        }\n\n        @Override\n        public SearchFeedItem getItem(int position) {\n            return searchFeedItems.get(position);\n        }\n\n        @Override\n        public long getItemId(int position) {\n            return position;\n        }\n\n        @Override\n        public View getView(final int position, View convertView, @NotNull final ViewGroup parent) {\n            final SearchFeedItem searchFeedItem = this.getItem(position);\n            if (convertView == null) {\n                cvh = new CustomViewHolder();\n                convertView = LayoutInflater.from(SearchActivity.this).inflate(R.layout.activity_search_list_item_feed, null);\n                cvh.feedIcon = convertView.findViewById(R.id.search_list_item_icon);\n                cvh.feedTitle = convertView.findViewById(R.id.search_list_item_title);\n                cvh.feedSummary = convertView.findViewById(R.id.search_list_item_summary);\n                cvh.feedUrl = convertView.findViewById(R.id.search_list_item_feed_url);\n                cvh.feedSubsVelocity = convertView.findViewById(R.id.search_list_item_sub_velocity);\n                cvh.feedLastUpdated = convertView.findViewById(R.id.search_list_item_last_updated);\n                cvh.feedSubState = convertView.findViewById(R.id.search_list_item_sub_state);\n                convertView.setTag(cvh);\n            } else {\n                cvh = (CustomViewHolder) convertView.getTag();\n            }\n            cvh.feedTitle.setText(searchFeedItem.getTitle());\n            if (!TextUtils.isEmpty(searchFeedItem.getDescription())) {\n                cvh.feedSummary.setVisibility(View.VISIBLE);\n                cvh.feedSummary.setText(searchFeedItem.getDescription());\n            } else {\n                cvh.feedSummary.setVisibility(View.GONE);\n                cvh.feedSummary.setText(\"\");\n            }\n//            KLog.e(\"当前view是：\" + position +\"  \" + convertView.getId() + \"\");\n//            Glide.with(SearchActivity.this).load(searchFeedItem.getVisualUrl()).centerCrop().into(cvh.feedIcon);\n            Glide.with(SearchActivity.this).load(searchFeedItem.getVisualUrl()).apply(options).into(cvh.feedIcon);\n\n            cvh.feedUrl.setText(searchFeedItem.getFeedId().replaceFirst(\"feed/\", \"\"));\n\n            cvh.feedSubsVelocity.setText( getResources().getQuantityString(R.plurals.search_result_followers, searchFeedItem.getSubscribers(), searchFeedItem.getSubscribers() ) + getString(R.string.search_result_articles, searchFeedItem.getVelocity()) );\n            if (searchFeedItem.getLastUpdated() != 0) {\n                cvh.feedLastUpdated.setText(getString(R.string.search_result_last_update_time, TimeUtil.format(searchFeedItem.getLastUpdated(), \"yyyy-MM-dd\")));\n            } else {\n                cvh.feedLastUpdated.setText(\"\");\n            }\n            if (CoreDB.i().feedDao().getById(App.i().getUser().getId(), searchFeedItem.getFeedId()) != null) {\n                cvh.feedSubState.setText(R.string.font_tick);\n            } else {\n                cvh.feedSubState.setText(R.string.font_add);\n            }\n            cvh.feedSubState.setOnClickListener(new View.OnClickListener() {\n                @Override\n                public void onClick(final View view) {\n                    if (CoreDB.i().feedDao().getById(App.i().getUser().getId(), searchFeedItem.getFeedId()) != null) {\n                        view.setClickable(false); // 防止重复点击\n                        cvh.feedSubState.setText(R.string.font_tick);\n                        App.i().getApi().unsubscribeFeed(searchFeedItem.getFeedId(), new CallbackX() {\n                            @Override\n                            public void onSuccess(Object result) {\n                                CoreDB.i().feedDao().deleteById(App.i().getUser().getId(), searchFeedItem.getFeedId());\n                                ((IconFontView) view).setText(R.string.font_add);\n                                view.setClickable(true);\n                            }\n\n                            @Override\n                            public void onFailure(Object error) {\n                                ToastUtils.show(getString(R.string.unsubscribe_failed,error));\n                                view.setClickable(true);\n                            }\n                        });\n                    } else {\n                        cvh.feedSubState.setText(R.string.font_add);\n                        showSelectFolder(view, searchFeedItem.getFeedId());\n                    }\n                }\n            });\n            return convertView;\n        }\n    }\n\n\n    private CustomViewHolder cvh;\n\n    public static class CustomViewHolder {\n        TextView feedTitle;\n        TextView feedSummary;\n        TextView feedUrl;\n        TextView feedSubsVelocity;\n        TextView feedLastUpdated;\n        IconFontView feedSubState;\n        ImageView feedIcon;\n    }\n\n    private void initToolbar() {\n        Toolbar toolbar = findViewById(R.id.search_toolbar);\n        setSupportActionBar(toolbar);\n        getSupportActionBar().setHomeButtonEnabled(true); // 这个小于4.0版本是默认为true，在4.0及其以上是false。该方法的作用：决定左上角的图标是否可以点击(没有向左的小图标)，true 可点\n        getSupportActionBar().setDisplayHomeAsUpEnabled(true); // 决定左上角图标的左侧是否有向左的小箭头，true 有小箭头\n        getSupportActionBar().setDisplayShowTitleEnabled(true);\n        // setDisplayShowHomeEnabled(true)   //使左上角图标是否显示，如果设成false，则没有程序图标，仅仅就个标题，否则，显示应用程序图标，对应id为android.R.id.home，对应ActionBar.DISPLAY_SHOW_HOME\n        // setDisplayShowCustomEnabled(true)  // 使自定义的普通View能在title栏显示，即actionBar.setCustomView能起作用，对应ActionBar.DISPLAY_SHOW_CUSTOM\n    }\n\n    public void onSearchFeedsClicked(View view) {\n        if (TextUtils.isEmpty(searchView.getText().toString())) {\n            ToastUtils.show(R.string.please_input_keyword);\n            return;\n        }\n        searchView.clearFocus();\n        InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);\n        imm.hideSoftInputFromWindow(searchView.getWindowToken(), 0);\n        searchAndLoadFeedsData();\n    }\n\n    public void onSearchLocalArtsClicked(View view) {\n        Intent intent = new Intent(SearchActivity.this, MainActivity.class);\n        KLog.e(\"要搜索的词是\" + searchView.getText().toString());\n        intent.putExtra(\"searchWord\", searchView.getText().toString());\n        this.setResult(App.ActivityResult_SearchLocalArtsToMain, intent);\n        this.finish();\n        overridePendingTransition(R.anim.in_from_bottom, R.anim.out_from_bottom);\n    }\n\n    @Override\n    protected Colorful.Builder buildColorful(Colorful.Builder mColorfulBuilder) {\n        ViewGroupSetter artListViewSetter = new ViewGroupSetter(listView);\n        // 绑定ListView的Item View中的news_title视图，在换肤时修改它的text_color属性\n        artListViewSetter.childViewTextColor(R.id.search_list_item_title, R.attr.lv_item_title_color);\n        artListViewSetter.childViewTextColor(R.id.search_list_item_summary, R.attr.lv_item_desc_color);\n        artListViewSetter.childViewTextColor(R.id.search_list_item_feed_url, R.attr.lv_item_desc_color);\n        artListViewSetter.childViewTextColor(R.id.search_list_item_sub_state, R.attr.lv_item_title_color);\n\n        artListViewSetter.childViewTextColor(R.id.search_intent_feeds, R.attr.lv_item_desc_color);\n        artListViewSetter.childViewTextColor(R.id.search_local_articles, R.attr.lv_item_desc_color);\n\n        artListViewSetter.childViewTextColor(R.id.search_list_item_sub_velocity, R.attr.lv_item_info_color);\n        artListViewSetter.childViewTextColor(R.id.search_list_item_last_updated, R.attr.lv_item_info_color);\n\n        mColorfulBuilder\n                .backgroundColor(R.id.search_root, R.attr.root_view_bg)\n                // 设置 toolbar\n                .backgroundColor(R.id.search_toolbar, R.attr.topbar_bg);\n        return mColorfulBuilder;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/SettingActivity.java",
    "content": "package me.wizos.loread.activity;\n\nimport android.content.ClipData;\nimport android.content.ClipboardManager;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.pm.PackageInfo;\nimport android.content.pm.PackageManager;\nimport android.graphics.Color;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.CompoundButton;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.widget.Toolbar;\nimport androidx.recyclerview.widget.LinearLayoutManager;\n\nimport com.afollestad.materialdialogs.DialogAction;\nimport com.afollestad.materialdialogs.MaterialDialog;\nimport com.afollestad.materialdialogs.simplelist.MaterialSimpleListAdapter;\nimport com.afollestad.materialdialogs.simplelist.MaterialSimpleListItem;\nimport com.hjq.toast.ToastUtils;\nimport com.kyleduo.switchbutton.SwitchButton;\nimport com.socks.library.KLog;\n\nimport java.util.List;\n\nimport butterknife.BindView;\nimport butterknife.ButterKnife;\nimport me.wizos.loread.App;\nimport me.wizos.loread.BuildConfig;\nimport me.wizos.loread.Contract;\nimport me.wizos.loread.R;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.User;\nimport me.wizos.loread.view.colorful.Colorful;\n\n/**\n * @author Wizos on 2016/3/10.\n */\n\npublic class SettingActivity extends BaseActivity {\n    protected static final String TAG = \"SettingActivity\";\n\n    private TextView clearBeforeDaySummary;\n\n    @Nullable\n    @BindView(R.id.setting_auto_sync_sb)\n    SwitchButton autoSyncSB;\n    @Nullable\n    @BindView(R.id.setting_auto_sync_on_wifi_sb)\n    SwitchButton autoSyncOnWifiSB;\n    @BindView(R.id.setting_auto_sync_on_wifi)\n    View autoSyncOnWifi;\n    @Nullable\n    @BindView(R.id.setting_auto_sync_frequency)\n    View autoSyncFrequency;\n\n    @BindView(R.id.setting_sync_frequency_summary)\n    TextView autoSyncFrequencySummary;\n\n    @BindView(R.id.setting_lab)\n    TextView lab;\n\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_setting);\n        ButterKnife.bind(this);\n        initToolbar();\n        initView();\n    }\n\n\n    private void toggleAutoSyncItem() {\n        if (App.i().getUser().isAutoSync()) {\n            autoSyncSB.setChecked(true);\n            autoSyncOnWifi.setVisibility(View.VISIBLE);\n            autoSyncFrequency.setVisibility(View.VISIBLE);\n            autoSyncOnWifiSB.setChecked(App.i().getUser().isAutoSyncOnlyWifi());\n        } else {\n            autoSyncSB.setChecked(false);\n            autoSyncOnWifi.setVisibility(View.GONE);\n            autoSyncFrequency.setVisibility(View.GONE);\n        }\n    }\n\n    private void initView() {\n        SwitchButton downImgWifi, sysBrowserOpenLink, autoToggleTheme;\n//        autoSyncSB = findViewById(R.id.setting_auto_sync_sb);\n//        autoSyncOnWifi.findViewById(R.id.setting_auto_sync_on_wifi_sb);\n//        autoSyncFrequency.findViewById(R.id.setting_auto_sync_on_wifi_sb);\n        toggleAutoSyncItem();\n        autoSyncSB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {\n            @Override\n            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {\n                User user = App.i().getUser();\n                user.setAutoSync(b);\n                CoreDB.i().userDao().update(user);\n                toggleAutoSyncItem();\n            }\n        });\n\n\n        downImgWifi = findViewById(R.id.setting_down_img_sb);\n        downImgWifi.setChecked(App.i().getUser().isDownloadImgOnlyWifi());\n\n        clearBeforeDaySummary = findViewById(R.id.setting_clear_day_summary);\n        clearBeforeDaySummary.setText(getResources().getString(R.string.clear_day_summary, String.valueOf(App.i().getUser().getCachePeriod())));\n\n        int autoSyncFrequency = App.i().getUser().getAutoSyncFrequency();\n        if (autoSyncFrequency >= 60) {\n            autoSyncFrequencySummary.setText(getResources().getString(R.string.xx_hour, autoSyncFrequency / 60 + \"\"));\n\n        } else {\n            autoSyncFrequencySummary.setText(getResources().getString(R.string.xx_minute, autoSyncFrequency + \"\"));\n        }\n        sysBrowserOpenLink = findViewById(R.id.setting_link_open_mode_sb);\n        sysBrowserOpenLink.setChecked(App.i().getUser().isOpenLinkBySysBrowser());\n\n        autoToggleTheme = findViewById(R.id.setting_auto_toggle_theme_sb);\n        autoToggleTheme.setChecked(App.i().getUser().isAutoToggleTheme());\n\n        TextView versionSummary = findViewById(R.id.setting_about_summary);\n        PackageManager manager = this.getPackageManager();\n        String title;\n        try {\n            PackageInfo info = manager.getPackageInfo(this.getPackageName(), 0);\n            title = info.versionName;\n        } catch (PackageManager.NameNotFoundException unused) {\n            title = \"\";\n        }\n        versionSummary.setText(title);\n\n\n        if (BuildConfig.DEBUG) {\n            lab.setVisibility(View.VISIBLE);\n            lab.setOnClickListener(new View.OnClickListener() {\n                @Override\n                public void onClick(View view) {\n                    Intent intent = new Intent(SettingActivity.this, LabActivity.class);\n                    startActivity(intent);\n                    overridePendingTransition(R.anim.fade_in, R.anim.fade_out);\n                }\n            });\n        }\n    }\n\n\n    public void onSBClick(View view) {\n        SwitchButton v = (SwitchButton) view;\n        KLog.i(\"点击\");\n\n        User user = App.i().getUser();\n        switch (v.getId()) {\n            case R.id.setting_link_open_mode_sb:\n                user.setOpenLinkBySysBrowser(v.isChecked());\n                break;\n            case R.id.setting_auto_toggle_theme_sb:\n                user.setAutoToggleTheme(v.isChecked());\n                break;\n            case R.id.setting_down_img_sb:\n                user.setDownloadImgOnlyWifi(v.isChecked());\n                break;\n            default:\n                break;\n        }\n        CoreDB.i().userDao().update(user);\n    }\n\n\n    public void onClickAutoSyncFrequencySelect(View view) {\n        final int[] minuteArray = this.getResources().getIntArray(R.array.setting_sync_frequency_minute);\n        int preSelectTimeFrequency = App.i().getUser().getAutoSyncFrequency();\n        int preSelectTimeFrequencyIndex = -1;\n\n        int num = minuteArray.length;\n        CharSequence[] timeDescItems = new CharSequence[num];\n        for (int i = 0; i < num; i++) {\n            if (minuteArray[i] >= 60) {\n                timeDescItems[i] = getResources().getString(R.string.xx_hour, minuteArray[i] / 60 + \"\");\n            } else {\n                timeDescItems[i] = getResources().getString(R.string.xx_minute, minuteArray[i] + \"\");\n            }\n            if (preSelectTimeFrequency == minuteArray[i]) {\n                preSelectTimeFrequencyIndex = i;\n            }\n        }\n\n        final CharSequence[] timeDescArray = timeDescItems;\n\n        new MaterialDialog.Builder(this)\n                .title(R.string.setting_sync_frequency_title)\n                .items(timeDescArray)\n                .itemsCallbackSingleChoice(preSelectTimeFrequencyIndex, new MaterialDialog.ListCallbackSingleChoice() {\n                    @Override\n                    public boolean onSelection(MaterialDialog dialog, View view, int which, CharSequence text) {\n                        User user = App.i().getUser();\n                        user.setAutoSyncFrequency(minuteArray[which]);\n                        //App.i().getUserBox().put(user);\n                        CoreDB.i().userDao().update(user);\n\n                        KLog.i(\"选择了\" + which);\n                        autoSyncFrequencySummary.setText(timeDescArray[which]);\n                        dialog.dismiss();\n                        return true; // allow selection\n                    }\n                })\n                .show();\n    }\n\n    public void showClearBeforeDay(View view) {\n        final int[] dayValueArray = this.getResources().getIntArray(R.array.setting_clear_day_dialog_item_array);\n        int preSelectClearBeforeDay = App.i().getUser().getCachePeriod();\n        int preSelectClearBeforeDayIndex = -1;\n\n        int num = dayValueArray.length;\n        CharSequence[] dayDescArrayTemp = new CharSequence[num];\n        for (int i = 0; i < num; i++) {\n            dayDescArrayTemp[i] = getResources().getString(R.string.clear_day_summary, dayValueArray[i] + \"\");\n            if (preSelectClearBeforeDay == dayValueArray[i]) {\n                preSelectClearBeforeDayIndex = i;\n            }\n        }\n\n        final CharSequence[] dayDescArray = dayDescArrayTemp;\n\n        new MaterialDialog.Builder(this)\n                .title(R.string.setting_clear_day_title)\n                .items(dayDescArray)\n                .itemsCallbackSingleChoice(preSelectClearBeforeDayIndex, new MaterialDialog.ListCallbackSingleChoice() {\n                    @Override\n                    public boolean onSelection(MaterialDialog dialog, View view, int which, CharSequence text) {\n                        User user = App.i().getUser();\n                        user.setCachePeriod(dayValueArray[which]);\n//                        App.i().getUserBox().put(user);\n                        CoreDB.i().userDao().update(user);\n\n                        clearBeforeDaySummary.setText(dayDescArray[which]);\n                        KLog.i(\"选择了\" + which);\n                        dialog.dismiss();\n                        return true;\n                    }\n                })\n                .show();\n    }\n\n\n    public void showAbout(View view) {\n        new MaterialDialog.Builder(this)\n                .title(R.string.setting_about_dialog_title)\n                .content(R.string.setting_about_dialog_content)\n//                    .positiveText(R.string.agree)\n//                    .negativeText(R.string.disagree)\n//                    .positiveColorRes(R.color.material_red_400)\n//                    .negativeColorRes(R.color.material_red_400)\n//                    .positiveColor(Color.WHITE)\n//                    .negativeColorAttr(android.R.attr.textColorSecondaryInverse)\n//                    .titleGravity(GravityEnum.CENTER)\n//                    .titleColorRes(R.color.material_red_400)\n//                    .contentColorRes(android.R.color.white)\n//                    .backgroundColorRes(R.color.material_blue_grey_800)\n//                    .dividerColorRes(R.color.material_teal_a400)\n//                    .btnSelector(R.drawable.md_btn_selector_custom, DialogAction.POSITIVE)\n//                    .theme(Theme.DARK)\n                .show();\n    }\n\n\n    /****************\n     *\n     * 发起添加群流程。群号：知微(106211435) 的 key 为： XPc8IGwXCDTPXItxM33eog5QLpLFdDrf\n     * 调用 joinQQGroup(XPc8IGwXCDTPXItxM33eog5QLpLFdDrf) 即可发起手Q客户端申请加群 知微(106211435)\n     *\n     * @param view 对应的控件\n     * @return 返回true表示呼起手Q成功，返回fals表示呼起失败\n     ******************/\n    public void joinQQGroup(View view) {\n        Intent intent = new Intent();\n        intent.setData(Uri.parse(\"mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26k%3D\" + \"XPc8IGwXCDTPXItxM33eog5QLpLFdDrf\"));\n        // 此Flag可根据具体产品需要自定义，如设置，则在加群界面按返回，返回手Q主界面，不设置，按返回会返回到呼起产品界面\n        // intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        try {\n            startActivity(intent);\n        } catch (Exception e) {\n            // 未安装手Q或安装的版本不支持\n            ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);\n            ClipData mClipData = ClipData.newPlainText(\"QQ\", \"106211435\");\n            // 将ClipData内容放到系统剪贴板里。\n            assert cm != null;\n            cm.setPrimaryClip(mClipData);\n            ToastUtils.show(getString(R.string.copy_success));\n        }\n    }\n\n    public void addAccount() {\n        Intent intent = new Intent(this, ProviderActivity.class);\n        startActivity(intent);\n        finish();\n        overridePendingTransition(R.anim.fade_in, R.anim.out_from_bottom);\n    }\n\n    public void escAccount() {\n        new MaterialDialog.Builder(SettingActivity.this)\n                .content(R.string.do_you_want_to_delete_data_of_this_account_after_logout)\n                .neutralText(R.string.cancel)\n                .negativeText(R.string.disagree)\n                .positiveText(R.string.agree)\n                .onPositive(new MaterialDialog.SingleButtonCallback() {\n                    @Override\n                    public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n                        dialog.dismiss();\n                        App.i().getKeyValue().remove(Contract.UID);\n                        App.i().clearApiData();\n                        Intent intent = new Intent(SettingActivity.this, ProviderActivity.class)\n                                .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);\n                        startActivity(intent);\n                        SettingActivity.this.finish();\n                        overridePendingTransition(R.anim.fade_in, R.anim.out_from_bottom);\n                    }\n                })\n                .onNegative(new MaterialDialog.SingleButtonCallback() {\n                    @Override\n                    public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n                        dialog.dismiss();\n                        App.i().getKeyValue().remove(Contract.UID);\n                        Intent intent = new Intent(SettingActivity.this, ProviderActivity.class)\n                                .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);\n                        startActivity(intent);\n                        SettingActivity.this.finish();\n                        overridePendingTransition(R.anim.fade_in, R.anim.out_from_bottom);\n                    }\n                })\n                .onNeutral(new MaterialDialog.SingleButtonCallback() {\n                    @Override\n                    public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n                        dialog.dismiss();\n                    }\n                })\n                .show();\n    }\n\n    public void onClickSwitchUser(View view) {\n        final List<User> users = CoreDB.i().userDao().loadAll();\n        KLog.i(\"点击切换账号：\" + users );\n        // 弹窗的适配器\n        MaterialSimpleListAdapter adapter = new MaterialSimpleListAdapter(new MaterialSimpleListAdapter.Callback() {\n            @Override\n            public void onMaterialListItemSelected(MaterialDialog dialog, int index, MaterialSimpleListItem item) {\n                dialog.dismiss();\n                int count = users.size();\n                if (index < count) {\n                    App.i().getKeyValue().putString(Contract.UID, users.get(index).getId());\n                    App.i().restartApp();\n                } else if (index == count) {\n                    addAccount();\n                } else if (index == count + 1) {\n                    escAccount();\n                }\n            }\n        });\n        int iconRefs = R.drawable.ic_rename;\n        for (User user : users) {\n            switch (user.getSource()) {\n                case Contract.PROVIDER_FEEDLY:\n                    iconRefs = R.drawable.logo_feedly;\n                    break;\n                case Contract.PROVIDER_INOREADER:\n                    iconRefs = R.drawable.logo_inoreader;\n                    break;\n                case Contract.PROVIDER_TINYRSS:\n                    iconRefs = R.drawable.logo_tinytinyrss;\n                    break;\n            }\n\n            adapter.add(new MaterialSimpleListItem.Builder(SettingActivity.this)\n                    .content( user.getUserName())\n                    .icon(iconRefs)\n                    .backgroundColor(Color.TRANSPARENT)\n                    .build());\n        }\n\n        adapter.add(new MaterialSimpleListItem.Builder(SettingActivity.this)\n                .content(R.string.add_account)\n                // .icon(R.drawable.ic_rename)\n                .backgroundColor(Color.TRANSPARENT)\n                .build());\n        adapter.add(new MaterialSimpleListItem.Builder(SettingActivity.this)\n                .content(R.string.esc_account)\n                //                .icon(R.drawable.ic_rename)\n                .backgroundColor(Color.TRANSPARENT)\n                .build());\n        new MaterialDialog.Builder(this)\n                .title(R.string.switch_account)\n                .adapter(adapter, new LinearLayoutManager(this))\n                .show();\n    }\n\n    private void initToolbar() {\n        Toolbar toolbar = findViewById(R.id.setting_toolbar);\n        setSupportActionBar(toolbar);\n        getSupportActionBar().setHomeButtonEnabled(true); // 这个小于4.0版本是默认为true，在4.0及其以上是false。该方法的作用：决定左上角的图标是否可以点击(没有向左的小图标)，true 可点\n        getSupportActionBar().setDisplayHomeAsUpEnabled(true); // 决定左上角图标的左侧是否有向左的小箭头，true 有小箭头\n        getSupportActionBar().setDisplayShowTitleEnabled(true);\n        // setDisplayShowHomeEnabled(true)   //使左上角图标是否显示，如果设成false，则没有程序图标，仅仅就个标题，否则，显示应用程序图标，对应id为android.R.id.home，对应ActionBar.DISPLAY_SHOW_HOME\n        // setDisplayShowCustomEnabled(true)  // 使自定义的普通View能在title栏显示，即actionBar.setCustomView能起作用，对应ActionBar.DISPLAY_SHOW_CUSTOM\n    }\n\n    public void onClickFeedback(View view) {\n        Intent intent = new Intent(SettingActivity.this, WebActivity.class);\n        intent.setData(Uri.parse(\"https://support.qq.com/products/15424\"));\n        startActivity(intent);\n        overridePendingTransition(R.anim.fade_in, R.anim.fade_out);\n    }\n\n\n\n    @Override\n    protected Colorful.Builder buildColorful(Colorful.Builder mColorfulBuilder) {\n        mColorfulBuilder\n//                .backgroundDrawable(R.id.swipe_layout, R.attr.root_view_bg)\n                // 设置view的背景图片\n                .backgroundColor(R.id.setting_coordinator, R.attr.root_view_bg)\n                // 设置 toolbar\n                .backgroundColor(R.id.setting_toolbar, R.attr.topbar_bg)\n//                .textColor(R.id.setting_toolbar_count, R.attr.topbar_fg)\n                // 设置文章信息\n//                .textColor(R.id.setting_sync_first_open_title, R.attr.setting_title)\n//                .textColor(R.id.setting_sync_all_starred_title, R.attr.setting_title)\n//                .textColor(R.id.setting_sync_frequency_title, R.attr.setting_title)\n//                .textColor(R.id.setting_sync_frequency_summary, R.attr.setting_tips)\n                .textColor(R.id.setting_clear_day_title, R.attr.setting_title)\n                .textColor(R.id.setting_clear_day_summary, R.attr.setting_tips)\n                .textColor(R.id.setting_down_img_title, R.attr.setting_title)\n\n//                .textColor(R.id.setting_scroll_mark_title, R.attr.setting_title)\n//                .textColor(R.id.setting_scroll_mark_tips, R.attr.setting_tips)\n//                .textColor(R.id.setting_order_tagfeed_title, R.attr.setting_title)\n//                .textColor(R.id.setting_order_tagfeed_tips, R.attr.setting_tips)\n//                .textColor(R.id.setting_link_open_mode_tips, R.attr.setting_tips)\n//                .textColor(R.id.setting_cache_path_starred_title, R.attr.setting_title)\n//                .textColor(R.id.setting_cache_path_starred_summary, R.attr.setting_tips)\n                .textColor(R.id.setting_link_open_mode_title, R.attr.setting_title)\n                .textColor(R.id.setting_license_title, R.attr.setting_title)\n                .textColor(R.id.setting_license_summary, R.attr.setting_tips)\n                .textColor(R.id.setting_about_title, R.attr.setting_title)\n                .textColor(R.id.setting_about_summary, R.attr.setting_tips);\n        return mColorfulBuilder;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/SplashActivity.java",
    "content": "package me.wizos.loread.activity;\n\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.text.TextUtils;\n\nimport com.socks.library.KLog;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.Contract;\nimport me.wizos.loread.R;\nimport me.wizos.loread.view.colorful.Colorful;\n\npublic class SplashActivity extends BaseActivity {\n    protected static final String TAG = \"SplashActivity\";\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        // 0.防止在该app已经启动的情况下，点击桌面图标还会再次进入该页面\n        // 例如第一次点击图标启动应用是启动首界面A，然后进入第二个界面B；按home键后，再次点击图标，进入的页面是A\n        // https://blog.csdn.net/gufeilong/article/details/72900365\n        if (!this.isTaskRoot()) {\n            Intent intent = getIntent();\n            if (intent != null) {\n                String action = intent.getAction();\n                if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(action)) {\n                    finish();\n                    return;\n                }\n            }\n        }\n//        // 1.检查是否需要更新，是则跳转只更新页面/弹出更新Dialog\n//        // 2.获取已登录的服务商\n//        // 2.1 未登录则跳转登录页(服务商选择页)\n//        // 2.2 检查登录授权时间，超时则跳转对应服务商的重新授权页\n//        // 2.3 加载一些相关参数、配置到内存？还是在用到的时候再去加载？\n//        // 3.跳转至文章列表页\n        Intent intent;\n        String uid = App.i().getKeyValue().getString(Contract.UID, null);\n        KLog.e(\"获取UID：\" + uid );\n        if ( TextUtils.isEmpty(uid) ) {\n            intent = new Intent(this, ProviderActivity.class);\n        } else {\n            intent = new Intent(this, MainActivity.class);\n        }\n        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);\n        startActivity(intent);\n        overridePendingTransition(R.anim.fade_in, R.anim.fade_out);\n        finish();\n    }\n\n    public Colorful.Builder buildColorful(Colorful.Builder mColorfulBuilder) {\n        return mColorfulBuilder;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/TTSActivity.java",
    "content": "package me.wizos.loread.activity;\n\nimport android.content.ComponentName;\nimport android.content.Intent;\nimport android.content.ServiceConnection;\nimport android.graphics.drawable.Drawable;\nimport android.os.Bundle;\nimport android.os.IBinder;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.view.animation.BounceInterpolator;\nimport android.widget.ImageView;\nimport android.widget.SeekBar;\nimport android.widget.TextView;\n\nimport androidx.appcompat.widget.Toolbar;\n\nimport com.freedom.lauzy.playpauseviewlib.PlayPauseView;\nimport com.hjq.toast.ToastUtils;\nimport com.noober.background.drawable.DrawableCreator;\nimport com.socks.library.KLog;\nimport com.yhao.floatwindow.constant.MoveType;\nimport com.yhao.floatwindow.constant.Screen;\nimport com.yhao.floatwindow.view.FloatWindow;\n\nimport me.wizos.loread.R;\nimport me.wizos.loread.service.AudioService;\nimport me.wizos.loread.utils.ScreenUtil;\nimport me.wizos.loread.view.colorful.Colorful;\n\npublic class TTSActivity extends BaseActivity {\n    int articleNo;\n    boolean isQueue;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_tts);\n        initToolbar();\n        initFloatWindow();\n        Intent intent = getIntent();\n        articleNo = intent.getIntExtra(\"articleNo\", 0);\n        isQueue = intent.getBooleanExtra(\"isQueue\",false);\n\n        // Broadcast\n        KLog.e(\"获取到要播报：\"  + isQueue);\n\n        playConnection = new PlayConnection();\n        intent = new Intent(this, AudioService.class);\n        intent.putExtra(\"articleNo\", articleNo);\n        intent.putExtra(\"isQueue\",isQueue);\n        startService(intent);\n        bindService(intent, playConnection, BIND_AUTO_CREATE);\n    }\n\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        //maHandler.removeCallbacksAndMessages(null);\n        if (playConnection != null) {\n            //退出应用后与service解除绑定\n            unbindService(playConnection);\n        }\n    }\n\n    private boolean isChangeProgress = false;\n    protected SeekBar seekBar;\n    protected TextView speedView;\n    protected TextView currTimeView;\n    protected TextView totalTimeView;\n    protected PlayPauseView playPauseView;\n    protected PlayConnection playConnection;\n    protected AudioService.AudioControlBinder audioControl;\n\n//    protected static Handler maHandler = new Handler();\n//    protected Runnable progressTask = new Runnable() {\n//        @Override\n//        public void run() {\n//            int currentPosition = musicControl.getCurrentPosition();\n//            int duration = musicControl.getDuration();\n//\n//            if (seekBar != null && !isChangeProgress) {\n//                seekBar.setProgress(currentPosition);\n//                seekBar.setSecondaryProgress(musicControl.getBufferedPercent() * duration / 100);\n//                seekBar.setMax(duration);\n//            }\n//            if (currTimeView != null && !isChangeProgress) {\n//                currTimeView.setText(TimeUtil.getTime(currentPosition));\n//                totalTimeView.setText(TimeUtil.getTime(duration));\n//            }\n//            //KLog.e(\"进度：\" + seekBar + \", \" + currTimeView + \" , \" + duration + \" = \"  + TimeUtil.getTime(duration));\n//            maHandler.postDelayed(progressTask, 1000);\n//        }\n//    };\n\n    public class PlayConnection implements ServiceConnection {\n        //服务启动完成后会进入到这个方法\n        @Override\n        public void onServiceConnected(ComponentName name, IBinder service) {\n            //获得service中的MyBinder\n            KLog.e(\"服务连接：onServiceConnected, musicControl: \" + audioControl);\n            initView(service);\n        }\n\n        @Override\n        public void onServiceDisconnected(ComponentName name) {\n            KLog.e(\"服务断开连接：onServiceDisconnected, musicControl: \" + audioControl);\n        }\n    }\n\n    public void initToolbar() {\n        Toolbar toolbar = findViewById(R.id.music_toolbar);\n        setSupportActionBar(toolbar);\n        // 这个小于4.0版本是默认为true，在4.0及其以上是false。该方法的作用：决定左上角的图标是否可以点击(没有向左的小图标)，true 可点\n        getSupportActionBar().setHomeButtonEnabled(true);\n        // 决定左上角图标的左侧是否有向左的小箭头，true 有小箭头\n        getSupportActionBar().setDisplayHomeAsUpEnabled(true);\n        getSupportActionBar().setDisplayShowTitleEnabled(false);\n        getSupportActionBar().setTitle(getString(R.string.music));\n        toolbar.setTitle(getString(R.string.music));\n        toolbar.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                TTSActivity.this.finish();\n            }\n        });\n    }\n\n    public void initView(IBinder service) {\n        audioControl = (AudioService.AudioControlBinder) service;\n        ImageView closeView = findViewById(R.id.music_close);\n        TextView titleView = findViewById(R.id.music_title);\n        playPauseView = findViewById(R.id.music_play_pause_view);\n        currTimeView = findViewById(R.id.currTime);\n        totalTimeView = findViewById(R.id.totalTime);\n        seekBar = findViewById(R.id.progressBar);\n        speedView = findViewById(R.id.music_speed);\n\n        titleView.setText(audioControl.getTitle());\n\n        if (audioControl.isPlaying()) {\n            playPauseView.play();\n            //maHandler.post(progressTask);\n        } else {\n            playPauseView.pause();\n//            currTimeView.setText(TimeUtil.getTime(musicControl.getCurrentPosition()));\n//            totalTimeView.setText(TimeUtil.getTime(musicControl.getDuration()));\n//            seekBar.setProgress(musicControl.getCurrentPosition());\n        }\n\n\n        audioControl.setPlayStatusListener(new AudioService.PlayStatusListener() {\n            @Override\n            public void onPlay() {\n                playPauseView.play();\n//                musicControl.setSpeed(App.i().getUser().getAudioSpeed());\n//                maHandler.postDelayed(progressTask, 1000);\n            }\n\n            @Override\n            public void onPause() {\n                playPauseView.pause();\n//                maHandler.removeCallbacks(progressTask);\n            }\n\n            @Override\n            public void onEnd() {\n                playPauseView.pause();\n//                maHandler.removeCallbacks(progressTask);\n                TTSActivity.this.finish();\n            }\n\n            @Override\n            public void onError(String cause) {\n                ToastUtils.show(\"系统出错：\" + cause);\n                playPauseView.pause();\n                //maHandler.removeCallbacks(progressTask);\n            }\n        });\n        playPauseView.setPlayPauseListener(new PlayPauseView.PlayPauseListener() {\n            @Override\n            public void play() {\n                audioControl.play();\n//                maHandler.removeCallbacks(progressTask);\n//                maHandler.postDelayed(progressTask, 1000);\n            }\n\n            @Override\n            public void pause() {\n//                maHandler.removeCallbacks(progressTask);\n                audioControl.pause();\n            }\n        });\n\n//        seekBar.setMax(musicControl.getDuration());\n//        seekBar.setProgress(musicControl.getCurrentPosition());\n//        seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {\n//            @Override\n//            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {\n//                if (fromUser && currTimeView != null) {\n//                    currTimeView.setText(TimeUtil.getTime(progress));\n//                }\n//            }\n//\n//            //开始触摸进度条，停止更新进度条\n//            @Override\n//            public void onStartTrackingTouch(SeekBar seekBar) {\n//                isChangeProgress = true;\n//            }\n//\n//            //停止触摸进度条\n//            @Override\n//            public void onStopTrackingTouch(SeekBar seekBar) {\n//                isChangeProgress = false;\n//                musicControl.seekTo(seekBar.getProgress());\n//            }\n//        });\n\n\n        closeView.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                //maHandler.removeCallbacks(progressTask);\n                // 关闭悬浮窗\n                FloatWindow.destroy();\n                // 关闭 serview\n                Intent intent2 = new Intent(TTSActivity.this, AudioService.class);\n                stopService(intent2);\n                // 关闭 activity\n                TTSActivity.this.finish();\n            }\n        });\n\n\n//        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {\n//            speedView.setVisibility(View.GONE);\n//        } else {\n//            speedView.setText(App.i().getUser().getAudioSpeed() + \"\");\n//            speedView.setOnClickListener(new View.OnClickListener() {\n//                @Override\n//                public void onClick(View v) {\n//                    new XPopup.Builder(Music1Activity.this)\n//                            .isCenterHorizontal(true) //是否与目标水平居中对齐\n//                            .offsetY(-10)\n//                            .hasShadowBg(true)\n//                            .popupAnimation(PopupAnimation.ScaleAlphaFromCenter)\n//                            .atView(speedView)  // 依附于所点击的View，内部会自动判断在上方或者下方显示\n//                            .asAttachList(new String[]{\"0.8\", \"1.0\", \"1.2\", \"1.5\", \"2.0\"},\n//                                    null,\n//                                    new OnSelectListener() {\n//                                        @Override\n//                                        public void onSelect(int which, String text) {\n//                                            musicControl.setSpeed(Float.parseFloat(text));\n//                                            User user = App.i().getUser();\n//                                            user.setAudioSpeed(Float.parseFloat(text));\n//                                            //App.i().getUserBox().put(user);\n//                                            CoreDB.i().userDao().update(user);\n//                                            speedView.setText(text);\n//                                        }\n//                                    })\n//                            .show();\n//                }\n//            });\n//        }\n    }\n\n\n    private void initFloatWindow() {\n        ImageView imageView = new ImageView(this);\n        imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);\n        imageView.setPadding(ScreenUtil.dp2px(10), ScreenUtil.dp2px(10), ScreenUtil.dp2px(10), ScreenUtil.dp2px(10));\n        imageView.setImageDrawable(getDrawable(R.drawable.ic_music));\n\n        //imageView.setBackground(getDrawable(R.drawable.shape_corners));\n        Drawable drawable = new DrawableCreator.Builder()\n//                .setUnPressedDrawable( getDrawable(R.color.bluePrimary) )\n                .setRipple(true, getResources().getColor(R.color.primary))\n                .setPressedSolidColor(getResources().getColor(R.color.primary), getResources().getColor(R.color.bluePrimary))\n                .setSolidColor(getResources().getColor(R.color.bluePrimary))\n                .setCornersRadius(ScreenUtil.dp2px(30))\n                .build();\n        imageView.setBackground(drawable);\n        imageView.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                Intent intent = new Intent(getApplicationContext(), TTSActivity.class);\n                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n                startActivity(intent);\n            }\n        });\n\n\n        FloatWindow\n                .with(getApplicationContext())\n                .setView(imageView)\n                .setWidth(Screen.width, 0.15f) //设置悬浮控件宽高\n                .setHeight(Screen.width, 0.15f)\n                .setX(Screen.width, 0.8f) //设置控件初始位置\n                .setY(Screen.height, 0.8f)\n                .setMoveType(MoveType.slide, 10, 10, 10, 10)\n                .setMoveStyle(500, new BounceInterpolator())\n                .setFilter(true, MainActivity.class, ArticleActivity.class)\n                .setDesktopShow(false)\n                .build();\n    }\n\n\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        if( item.getItemId() == android.R.id.home ){\n            this.finish();\n            overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    @Override\n    protected Colorful.Builder buildColorful(Colorful.Builder mColorfulBuilder) {\n        return mColorfulBuilder;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/WebActivity.java",
    "content": "package me.wizos.loread.activity;\n\nimport android.annotation.SuppressLint;\nimport android.content.ClipData;\nimport android.content.ClipboardManager;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.pm.ActivityInfo;\nimport android.content.pm.PackageManager;\nimport android.content.res.Configuration;\nimport android.graphics.Bitmap;\nimport android.net.Uri;\nimport android.net.http.SslError;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.text.InputType;\nimport android.text.TextUtils;\nimport android.view.KeyEvent;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewConfiguration;\nimport android.webkit.CookieManager;\nimport android.webkit.SslErrorHandler;\nimport android.webkit.WebResourceError;\nimport android.webkit.WebResourceRequest;\nimport android.webkit.WebResourceResponse;\nimport android.webkit.WebSettings;\nimport android.webkit.WebView;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.ActionBar;\nimport androidx.appcompat.widget.Toolbar;\nimport androidx.coordinatorlayout.widget.CoordinatorLayout;\n\nimport com.afollestad.materialdialogs.DialogAction;\nimport com.afollestad.materialdialogs.MaterialDialog;\nimport com.google.android.material.appbar.AppBarLayout;\nimport com.hjq.toast.ToastUtils;\nimport com.just.agentweb.AgentWeb;\nimport com.just.agentweb.DefaultWebClient;\nimport com.just.agentweb.NestedScrollAgentWebView;\nimport com.just.agentweb.WebChromeClient;\nimport com.just.agentweb.WebViewClient;\nimport com.socks.library.KLog;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.Map;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.Contract;\nimport me.wizos.loread.R;\nimport me.wizos.loread.bridge.WebBridge;\nimport me.wizos.loread.config.AdBlock;\nimport me.wizos.loread.config.LinkRewriteConfig;\nimport me.wizos.loread.config.NetworkUserAgentConfig;\nimport me.wizos.loread.utils.ScreenUtil;\nimport me.wizos.loread.utils.VideoInjectUtil;\nimport me.wizos.loread.view.colorful.Colorful;\nimport me.wizos.loread.view.webview.DownloadListenerS;\nimport me.wizos.loread.view.webview.LongClickPopWindow;\n\nimport static me.wizos.loread.Contract.HTTP;\nimport static me.wizos.loread.Contract.HTTPS;\nimport static me.wizos.loread.Contract.SCHEMA_HTTP;\nimport static me.wizos.loread.Contract.SCHEMA_HTTPS;\nimport static me.wizos.loread.Contract.SCHEMA_LOREAD;\n\n/**\n * 内置的 webView 页面，用来相应 a，iframe 的跳转内容\n * @author Wizos\n */\npublic class WebActivity extends BaseActivity implements WebBridge {\n    private AgentWeb agentWeb;\n    private Toolbar mToolbar;\n    private AppBarLayout appBarLayout;\n    private CoordinatorLayout containerLayout;\n    private String originalUrl;\n    private String receivedUrl;\n    private static Handler handler = new Handler();\n    private int downX, downY;\n    private boolean isPortrait = true; //是否为竖屏\n\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_web);\n        containerLayout = this.findViewById(R.id.web_root);\n        appBarLayout = this.findViewById(R.id.web_appBarLayout);\n        mToolbar = this.findViewById(R.id.web_toolbar);\n        mToolbar.setTitle(getString(R.string.loading));\n        setSupportActionBar(mToolbar);\n        // 这个小于4.0版本是默认为true，在4.0及其以上是false。该方法的作用：决定左上角的图标是否可以点击(没有向左的小图标)，true 可点\n        getSupportActionBar().setHomeButtonEnabled(true);\n        // 决定左上角图标的左侧是否有向左的小箭头，true 有小箭头\n        getSupportActionBar().setDisplayHomeAsUpEnabled(true);\n        getSupportActionBar().setDisplayShowTitleEnabled(true);\n\n        if (savedInstanceState != null) {\n            onRecoveryInstanceState(savedInstanceState);\n        }\n\n        originalUrl = getIntent().getDataString();\n        // 补救，获取 originalUrl\n        if (TextUtils.isEmpty(originalUrl)) {\n            originalUrl = getIntent().getStringExtra(Intent.EXTRA_TEXT);\n        }\n\n\n        String newUrl = LinkRewriteConfig.i().getRedirectUrl(originalUrl);\n        KLog.i(\"获取到链接，准备跳转B：\" + originalUrl + \", newUrl = \" + newUrl);\n        if (!TextUtils.isEmpty(newUrl)) {\n            originalUrl =  newUrl;\n        }\n\n        mToolbar.setSubtitle(originalUrl);\n\n        initWebView(originalUrl);\n\n        mToolbar.setOnClickListener(view -> {\n            if (handler.hasMessages(App.MSG_DOUBLE_TAP) && agentWeb != null) {\n                handler.removeMessages(App.MSG_DOUBLE_TAP);\n                agentWeb.getWebCreator().getWebView().scrollTo(0, 0);\n            } else {\n                handler.sendEmptyMessageDelayed(App.MSG_DOUBLE_TAP, ViewConfiguration.getDoubleTapTimeout());\n            }\n        });\n    }\n\n\n    @SuppressLint({\"ClickableViewAccessibility\", \"SetJavaScriptEnabled\"})\n    private void initWebView(String link) {\n        CoordinatorLayout.LayoutParams lp = new CoordinatorLayout.LayoutParams(-1, -1);\n        lp.setBehavior(new AppBarLayout.ScrollingViewBehavior());\n\n        agentWeb = AgentWeb.with(this)\n                .setAgentWebParent(containerLayout, -1, lp)//lp记得设置behavior属性\n//                .setAgentWebParent( containerLayout, new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))//传入AgentWeb的父控件。\n                .useDefaultIndicator(-1, 3)//设置进度条颜色与高度，-1为默认值，高度为2，单位为dp。\n                .setWebView(new NestedScrollAgentWebView(this))\n                .setWebViewClient(mWebViewClient)//WebViewClient ， 与 WebView 使用一致 ，但是请勿获取WebView调用setWebViewClient(webViewScroll)方法了,会覆盖AgentWeb DefaultWebClient,同时相应的中间件也会失效。\n                .setWebChromeClient(mWebChromeClient) //WebChromeClient\n                .setSecurityType(AgentWeb.SecurityType.STRICT_CHECK) //严格模式 Android 4.2.2 以下会放弃注入对象 ，使用AgentWebView没影响。\n                .setOpenOtherPageWays(DefaultWebClient.OpenOtherPageWays.ASK)//打开其他应用时，弹窗咨询用户是否前往其他应用\n                .addJavascriptInterface(WebBridge.TAG, WebActivity.this)\n//                .setAgentWebWebSettings(getSettings())//设置 IAgentWebSettings。\n//                .setPermissionInterceptor(mPermissionInterceptor) //权限拦截 2.0.0 加入。\n//                .setAgentWebUIController(new UIController(getActivity())) //自定义UI  AgentWeb3.0.0 加入。\n                .setMainFrameErrorView(R.layout.agentweb_error_page, -1) //参数1是错误显示的布局，参数2点击刷新控件ID -1表示点击整个布局都刷新， AgentWeb 3.0.0 加入。\n//                .setDownloadListener(mDownloadListener) // 4.0.0 删除该API//下载回调\n//                .openParallelDownload()// 4.0.0删除该api 打开并行下载 , 默认串行下载。 请通过AgentWebDownloader#Extra实现并行下载\n//                .setNotifyIcon(R.drawable.ic_file_download_black_24dp) 4.0.0删除该api //下载通知图标。4.0.0后的版本请通过AgentWebDownloader#Extra修改icon\n                .setOpenOtherPageWays(DefaultWebClient.OpenOtherPageWays.DISALLOW)//打开其他页面时，弹窗质询用户前往其他应用 AgentWeb 3.0.0 加入。\n                .interceptUnkownUrl() //拦截找不到相关页面的Url AgentWeb 3.0.0 加入。\n                .createAgentWeb()//创建AgentWeb。\n                .ready()//设置 WebSettings。\n                .go(link); //WebView载入该url地址的页面并显示。\n//        agentWeb.getWebCreator().getWebView().setHorizontalScrollBarEnabled(false);\n        agentWeb.getWebCreator().getWebView().getSettings().setTextZoom(100);\n        // 设置最小的字号，默认为8\n        agentWeb.getWebCreator().getWebView().getSettings().setMinimumFontSize(10);\n        // 设置最小的本地字号，默认为8\n        agentWeb.getWebCreator().getWebView().getSettings().setMinimumLogicalFontSize(10);\n\n        // 设置此属性，可任意比例缩放\n        agentWeb.getWebCreator().getWebView().getSettings().setUseWideViewPort(true);\n        // 缩放至屏幕的大小：如果webview内容宽度大于显示区域的宽度,那么将内容缩小,以适应显示区域的宽度, 默认是false\n        agentWeb.getWebCreator().getWebView().getSettings().setLoadWithOverviewMode(true);\n        // NARROW_COLUMNS 适应内容大小 ， SINGLE_COLUMN 自适应屏幕\n        agentWeb.getWebCreator().getWebView().getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);\n\n        //缩放操作\n        agentWeb.getWebCreator().getWebView().getSettings().setSupportZoom(true); //支持缩放，默认为true。是下面那个的前提。\n        agentWeb.getWebCreator().getWebView().getSettings().setBuiltInZoomControls(true); //设置内置的缩放控件。若为false，则该WebView不可缩放\n        agentWeb.getWebCreator().getWebView().getSettings().setDisplayZoomControls(false); //隐藏原生的缩放控件\n\n        agentWeb.getWebCreator().getWebView().getSettings().setDefaultTextEncodingName(StandardCharsets.UTF_8.name());\n\n        agentWeb.getWebCreator().getWebView().getSettings().setJavaScriptEnabled(true);\n        // 支持通过js打开新的窗口\n        agentWeb.getWebCreator().getWebView().getSettings().setJavaScriptCanOpenWindowsAutomatically(false);\n\n        /* 缓存 */\n        agentWeb.getWebCreator().getWebView().getSettings().setDomStorageEnabled(true); // 临时简单的缓存（必须保留，否则无法播放优酷视频网页，其他的可以）\n        agentWeb.getWebCreator().getWebView().getSettings().setAppCacheEnabled(true); // 支持H5的 application cache 的功能\n        // webSettings.setDatabaseEnabled(true);  // 支持javascript读写db\n        //根据cache-control获取数据。\n        agentWeb.getWebCreator().getWebView().getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);\n\n        // 通过 file url 加载的 Javascript 读取其他的本地文件 .建议关闭\n        agentWeb.getWebCreator().getWebView().getSettings().setAllowFileAccessFromFileURLs(false);\n        // 允许通过 file url 加载的 Javascript 可以访问其他的源，包括其他的文件和 http，https 等其他的源\n        agentWeb.getWebCreator().getWebView().getSettings().setAllowUniversalAccessFromFileURLs(false);\n        // 允许访问文件\n        agentWeb.getWebCreator().getWebView().getSettings().setAllowFileAccess(true);\n\n        // 保存表单数据\n        agentWeb.getWebCreator().getWebView().getSettings().setSaveFormData(true);\n        agentWeb.getWebCreator().getWebView().getSettings().setSavePassword(true);\n\n        // 允许在Android 5.0上 Webview 加载 Http 与 Https 混合内容。作者：Wing_Li，链接：https://www.jianshu.com/p/3fcf8ba18d7f\n        agentWeb.getWebCreator().getWebView().getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);\n\n        CookieManager instance = CookieManager.getInstance();\n        instance.setAcceptCookie(true);\n        instance.setAcceptThirdPartyCookies(agentWeb.getWebCreator().getWebView(), true);\n        CookieManager.setAcceptFileSchemeCookies(true);\n\n        agentWeb.getWebCreator().getWebView().getSettings().setMediaPlaybackRequiresUserGesture(true);\n\n        /**\n         * https://www.jianshu.com/p/6e38e1ef203a\n         * 让 WebView 支持文件下载，主要思路有：1、跳转浏览器下载；2、使用系统的下载服务；3、自定义下载任务\n         */\n        agentWeb.getWebCreator().getWebView().setDownloadListener(\n                new DownloadListenerS(WebActivity.this).setWebView(agentWeb.getWebCreator().getWebView())\n        );\n\n        String guessUserAgent = NetworkUserAgentConfig.i().guessUserAgentByUrl(link);\n        if (!TextUtils.isEmpty(guessUserAgent)) {\n            agentWeb.getWebCreator().getWebView().getSettings().setUserAgentString(guessUserAgent);\n        }\n\n        agentWeb.getWebCreator().getWebView().setOnTouchListener(new View.OnTouchListener() {\n            @Override\n            public boolean onTouch(View arg0, MotionEvent arg1) {\n                downX = (int) arg1.getX();\n                downY = (int) arg1.getY();\n                return false;\n            }\n        });\n        agentWeb.getWebCreator().getWebView().setOnLongClickListener(new View.OnLongClickListener() {\n            @Override\n            public boolean onLongClick(View webView) {\n                final WebView.HitTestResult result = ((WebView) webView).getHitTestResult();\n                if (null == result) {\n                    return false;\n                }\n                int type = result.getType();\n                if (type == WebView.HitTestResult.UNKNOWN_TYPE) {\n                    return false;\n                }\n\n//                // 这里可以拦截很多类型，我们只处理超链接就可以了\n//                final LongClickPopWindow webViewLongClickedPopWindow =\n//                        new LongClickPopWindow(WebActivity.this, status, ScreenUtil.dp2px(WebActivity.this,120), ScreenUtil.dp2px(WebActivity.this,90));\n//                webViewLongClickedPopWindow.showAtLocation(webView, Gravity.TOP|Gravity.LEFT, downX, downY + 10);\n                new LongClickPopWindow(WebActivity.this, (WebView) webView, ScreenUtil.dp2px(WebActivity.this, 120), ScreenUtil.dp2px(WebActivity.this, 130), downX, downY + 10);\n\n                return true;\n            }\n        });\n    }\n\n\n    @Override\n    public boolean onKeyDown(int keyCode, KeyEvent event) {\n        // 后者为短期内按下的次数\n        if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {\n            if (agentWeb.getWebCreator().getWebView().canGoBack()) {\n                if (WebActivity.this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {\n                    portrait();\n                }\n                agentWeb.getWebCreator().getWebView().stopLoading();\n                agentWeb.back();\n            } else {\n                exit();\n            }\n            return true;\n        }\n        return super.onKeyDown(keyCode, event);\n    }\n\n\n    private static final int UI_ANIMATION_DELAY = 300;\n    private final Runnable mHidePart2Runnable = new Runnable() {\n        @SuppressLint(\"InlinedApi\")\n        @Override\n        public void run() {\n            KLog.e(\"屏幕：隐藏状态\");\n\n            AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) appBarLayout.getChildAt(0).getLayoutParams();\n            KLog.e(\"屏幕：隐藏状态\" + containerLayout.getSystemUiVisibility());\n            params.setScrollFlags(0);\n\n            ActionBar actionBar = getSupportActionBar();\n            if (actionBar != null) {\n                actionBar.hide();\n            }\n\n//            View.SYSTEM_UI_FLAG_LOW_PROFILE                    状态栏显示处于低能显示状态(low profile模式)，状态栏上一些图标显示会被隐藏。\n//            View.SYSTEM_UI_FLAG_FULLSCREEN                     隐藏状态栏：全屏显示，但状态栏不会被隐藏覆盖，状态栏依然可见，Activity顶端布局部分会被状态遮住。\n//            View.SYSTEM_UI_FLAG_HIDE_NAVIGATION                隐藏导航栏\n//            View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN              布局占用状态栏区域\n//            View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION         布局占用导航栏区域\n//            View.SYSTEM_UI_FLAG_LAYOUT_STABLE                  稳定布局，防止系统栏隐藏时内容区域大小发生变化\n//            View.SYSTEM_UI_FLAG_IMMERSIVE                      沉浸式\n//            View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY               粘性沉浸式：向内滑动的操作会让系统栏临时显示\n            containerLayout.setFitsSystemWindows(false);\n            containerLayout.setSystemUiVisibility(\n//                            | View.SYSTEM_UI_FLAG_IMMERSIVE\n                    View.SYSTEM_UI_FLAG_LOW_PROFILE\n                            | View.SYSTEM_UI_FLAG_LAYOUT_STABLE\n                            | View.SYSTEM_UI_FLAG_FULLSCREEN // landscape status bar\n                            | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // landscape nav bar\n                            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\n                            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION\n                            | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY\n            );\n        }\n    };\n    private final Runnable mShowPart2Runnable = new Runnable() {\n        @Override\n        public void run() {\n            // Delayed display of UI elements\n            ActionBar actionBar = getSupportActionBar();\n            if (actionBar != null) {\n                actionBar.show();\n            }\n            AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) appBarLayout.getChildAt(0).getLayoutParams();\n            params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);\n            containerLayout.setFitsSystemWindows(true);\n            containerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);\n        }\n    };\n\n\n    @Override\n    protected void onSaveInstanceState(Bundle outState) {\n        outState.putBoolean(Contract.isPortrait, isPortrait);\n        KLog.i(\"自动保存，是否为竖屏：\" + isPortrait);\n        super.onSaveInstanceState(outState);\n    }\n\n    private void onRecoveryInstanceState(@NonNull Bundle outState) {\n        if (outState.getBoolean(Contract.isPortrait, true)) {\n            portrait();\n        } else {\n            landscape();\n        }\n    }\n\n    /**\n     * 切换为横屏\n     */\n    @SuppressLint(\"SourceLockedOrientationActivity\")\n    private void landscape() {\n        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);\n        isPortrait = false;\n        // Schedule a runnable to remove the status and navigation bar after a delay\n        handler.removeCallbacks(mShowPart2Runnable);\n        handler.postDelayed(mHidePart2Runnable, UI_ANIMATION_DELAY);\n    }\n\n    /**\n     * 切换为竖屏\n     */\n    @SuppressLint(\"SourceLockedOrientationActivity\")\n    private void portrait() {\n        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);\n        isPortrait = true;\n        // Schedule a runnable to display UI elements after a delay\n        handler.removeCallbacks(mHidePart2Runnable);\n        handler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY);\n    }\n\n\n    @Override\n    public void log(String msg) {\n        KLog.e(WebBridge.TAG, msg);\n    }\n\n    @Override\n    public void toggleScreenOrientation() {\n        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {\n            landscape();\n        } else {\n            portrait();\n        }\n    }\n\n    protected WebChromeClient mWebChromeClient = new WebChromeClient() {\n        @Override\n        public void onReceivedTitle(WebView view, String title) {\n            super.onReceivedTitle(view, title);\n            if (!TextUtils.isEmpty(title)) {\n                mToolbar.setTitle(title);\n                receivedUrl = agentWeb.getWebCreator().getWebView().getUrl();\n                mToolbar.setSubtitle(receivedUrl);\n            }\n        }\n    };\n\n\n    protected WebViewClient mWebViewClient = new WebViewClient() {\n        @Override\n        public WebResourceResponse shouldInterceptRequest(WebView view, final WebResourceRequest request) {\n            String scheme = request.getUrl().getScheme().trim();\n            if (scheme.equalsIgnoreCase(HTTP) || scheme.equalsIgnoreCase(HTTPS)) {\n                String url = request.getUrl().toString();\n                if (AdBlock.i().isAd(url)) {\n                    // 有广告的请求数据，我们直接返回空数据，注：不能直接返回null\n                    return new WebResourceResponse(null, null, null);\n                }\n\n                String newUrl = LinkRewriteConfig.i().getRedirectUrl(url);\n                // KLog.e(\"重定向地址：\" + url + \" , \" + newUrl);\n                if(!TextUtils.isEmpty(newUrl) && !url.equalsIgnoreCase(newUrl)){\n                    return super.shouldInterceptRequest(view, new WebResourceRequest() {\n                        @Override\n                        public Uri getUrl() {\n                            return Uri.parse(newUrl);\n                        }\n                        @SuppressLint(\"NewApi\")\n                        @Override\n                        public boolean isRedirect(){\n                            return true;\n                        }\n                        @SuppressLint(\"NewApi\")\n                        @Override\n                        public boolean isForMainFrame() {\n                            return request.isForMainFrame();\n                        }\n                        @SuppressLint(\"NewApi\")\n                        @Override\n                        public boolean hasGesture() {\n                            return request.hasGesture();\n                        }\n                        @SuppressLint(\"NewApi\")\n                        @Override\n                        public String getMethod() {\n                            return request.getMethod();\n                        }\n                        @SuppressLint(\"NewApi\")\n                        @Override\n                        public Map<String, String> getRequestHeaders() {\n                            return request.getRequestHeaders();\n                        }\n                    });\n                }\n            }\n            return super.shouldInterceptRequest(view, request);\n        }\n\n        @Override\n        public boolean shouldOverrideUrlLoading(final WebView view, WebResourceRequest request) {\n            // 优酷想唤起自己应用播放该视频，下面拦截地址返回true则会在应用内 H5 播放，禁止优酷唤起播放该视频。\n            // 如果返回false ， DefaultWebClient 会根据intent协议处理该地址，首先匹配该应用存不存在，\n            // 如果存在，唤起该应用播放，如果不存在，则跳到应用市场下载该应用.\n            String url = request.getUrl().toString();\n\n            KLog.i(\"地址：\" + url);\n            if (url.startsWith(SCHEMA_HTTP) || url.startsWith(SCHEMA_HTTPS)) {\n                return false;\n            }\n            //其他的URL则会开启一个Acitity然后去调用原生APP\n            final Intent in = new Intent(Intent.ACTION_VIEW, Uri.parse(url));\n            if (url.startsWith(SCHEMA_LOREAD)) {\n                startActivity(in);\n                finish();\n            }else if (in.resolveActivity(getPackageManager()) != null) {\n                in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);\n                String name = null;\n                try {\n                    name = \"\" + getPackageManager().getApplicationLabel(getPackageManager().getApplicationInfo(in.resolveActivity(getPackageManager()).getPackageName(), PackageManager.GET_META_DATA));\n                } catch (PackageManager.NameNotFoundException e) {\n                    KLog.e(R.string.unable_to_find_app);\n                    e.printStackTrace();\n                }\n                if (TextUtils.isEmpty(name)) {\n                    name = getString(R.string.corresponding);\n                }\n                new MaterialDialog.Builder(WebActivity.this)\n                        .content(R.string.do_you_want_to_jump_the_application, name)\n                        .negativeText(R.string.disagree)\n                        .positiveText(R.string.agree)\n                        .onPositive(new MaterialDialog.SingleButtonCallback() {\n                            @Override\n                            public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n                                startActivity(in);\n                                finish();\n                            }\n                        })\n                        .show();\n            }\n            return true;\n        }\n\n        @Override\n        public void onPageStarted(WebView webView, String url, Bitmap favicon) {\n            KLog.i(\"onPageStarted = \" + url);\n            super.onPageStarted(webView, url, favicon);\n\n            if (itemRefresh != null) {\n                itemRefresh.setVisible(false);\n            }\n            if (itemStop != null) {\n                itemStop.setVisible(true);\n            }\n        }\n\n        @Override\n        public void onPageFinished(WebView view, String url) {\n            super.onPageFinished(view, url);\n            // 注入视频全屏js\n            view.loadUrl(VideoInjectUtil.fullScreenJsFun(url));\n            if (itemStop != null) {\n                itemStop.setVisible(false);\n            }\n            if (itemRefresh != null) {\n                itemRefresh.setVisible(true);\n            }\n        }\n\n        @Override\n        public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {\n            super.onReceivedHttpError(view, request, errorResponse);\n            //KLog.e(\"接受http错误 = \" + request.getUrl() + errorResponse);\n        }\n\n        @Override\n        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {\n            //KLog.e(\"接受Ssl错误 = \" + view.getUrl() + error);\n            handler.proceed();//接受证书\n        }\n\n        @Override\n        public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {\n            super.onReceivedError(view, request, error);\n            //KLog.e(\"接受错误 = \" + request.getUrl() + error);\n        }\n    };\n\n    MenuItem itemRefresh, itemStop;\n\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_web, menu);\n        itemRefresh = menu.findItem(R.id.web_menu_refresh);\n        itemStop = menu.findItem(R.id.web_menu_stop);\n        return true;\n    }\n\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        switch (item.getItemId()) {\n            //监听左上角的返回箭头\n            case android.R.id.home:\n                exit();\n                break;\n            case R.id.web_menu_user_agent:\n                final ArrayList<String> uaTitle = new ArrayList<>();\n                String holdUA = NetworkUserAgentConfig.i().getHoldUserAgent();\n                uaTitle.add(getString(R.string.default_x));\n                int i = 0;\n                int selected = 0;\n                for (Map.Entry<String, String> entry : NetworkUserAgentConfig.i().getUserAgents().entrySet()) {\n                    uaTitle.add(entry.getKey());\n                    if(entry.getKey().equals(holdUA)){\n                        selected = i;\n                    }\n                    i++;\n                    KLog.e(\"标题：\" + entry.getKey());\n                }\n\n\n                int finalSelected = selected;\n                new MaterialDialog.Builder(WebActivity.this)\n                        .title(R.string.select_user_agent)\n                        .items(uaTitle)\n                        .itemsCallbackSingleChoice(selected + 1, new MaterialDialog.ListCallbackSingleChoice() {\n                            @Override\n                            public boolean onSelection(MaterialDialog dialog, View view, final int which, CharSequence text) {\n                                // 默认\n                                if (which == 0) {\n                                    NetworkUserAgentConfig.i().setHoldUserAgent(null);\n                                }\n                                // 手动选择项\n                                else if (which - 1 != finalSelected) {\n                                    NetworkUserAgentConfig.i().setHoldUserAgent(text.toString());\n\n                                }\n                                NetworkUserAgentConfig.i().save();\n                                agentWeb.getWebCreator().getWebView().getSettings().setUserAgentString(NetworkUserAgentConfig.i().guessUserAgentByUrl(receivedUrl));\n                                agentWeb.getWebCreator().getWebView().reload();\n//                                    KLog.e(\"默认的UA是：\" + agentWeb.getWebCreator().getWebView().getSettings().getUserAgentString() );\n                                dialog.dismiss();\n                                return true;\n                            }\n                        })\n                        .neutralText(R.string.custom_user_agent)\n                        .onNeutral(new MaterialDialog.SingleButtonCallback() {\n                            @Override\n                            public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n                                new MaterialDialog.Builder(WebActivity.this)\n                                        .title(R.string.enter_user_agent)\n                                        .inputType(InputType.TYPE_CLASS_TEXT)\n                                        .inputRange(12, 200)\n                                        .input(null, agentWeb.getWebCreator().getWebView().getSettings().getUserAgentString(), new MaterialDialog.InputCallback() {\n                                            @Override\n                                            public void onInput(@NotNull MaterialDialog dialog, CharSequence input) {\n                                                NetworkUserAgentConfig.i().setHoldUserAgent(getString(R.string.custom));\n                                                NetworkUserAgentConfig.i().getUserAgents().put(getString(R.string.custom),input.toString());\n                                                NetworkUserAgentConfig.i().save();\n                                                KLog.e(\"当前输入的是：\" + input.toString());\n                                                agentWeb.getWebCreator().getWebView().getSettings().setUserAgentString(NetworkUserAgentConfig.i().guessUserAgentByUrl(receivedUrl));\n                                                agentWeb.getWebCreator().getWebView().reload();\n                                            }\n                                        })\n                                        .positiveText(R.string.confirm)\n                                        .negativeText(android.R.string.cancel)\n                                        .neutralText(R.string.remove_custom_user_agent)\n                                        .neutralColor(WebActivity.this.getResources().getColor(R.color.material_red_400))\n                                        .onNeutral(new MaterialDialog.SingleButtonCallback() {\n                                            @Override\n                                            public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n                                                NetworkUserAgentConfig.i().setHoldUserAgent(getString(R.string.custom));\n                                                NetworkUserAgentConfig.i().getUserAgents().remove(getString(R.string.custom));\n                                                NetworkUserAgentConfig.i().save();\n                                            }\n                                        })\n                                        .show();\n                            }\n                        })\n                        .show();\n                break;\n            case R.id.web_menu_open_by_sys:\n                Intent intent = new Intent(Intent.ACTION_VIEW);\n                if (!TextUtils.isEmpty(receivedUrl)) {\n                    intent.setData(Uri.parse(receivedUrl));\n                } else {\n                    intent.setData(Uri.parse(originalUrl));\n                }\n                startActivity(intent);\n                break;\n            case R.id.web_menu_copy_link:\n                //获取剪贴板管理器：\n                ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);\n                ClipData mClipData;\n                if (!TextUtils.isEmpty(receivedUrl)) {\n                    // 创建普通字符型ClipData\n                    mClipData = ClipData.newRawUri(agentWeb.getWebCreator().getWebView().getTitle(), Uri.parse(receivedUrl));\n                } else {\n                    // 创建普通字符型ClipData\n                    mClipData = ClipData.newRawUri(agentWeb.getWebCreator().getWebView().getTitle(), Uri.parse(originalUrl));\n                }\n//                ClipData mClipData = ClipData.newPlainText(\"url\",webViewS.getUrl());\n                // 将ClipData内容放到系统剪贴板里。\n                cm.setPrimaryClip(mClipData);\n                ToastUtils.show(getString(R.string.copy_success));\n                break;\n            case R.id.web_menu_share:\n                Intent sendIntent = new Intent(Intent.ACTION_SEND);\n                sendIntent.setType(\"text/plain\");\n\n                sendIntent.putExtra(Intent.EXTRA_SUBJECT, mToolbar.getTitle());\n//                sendIntent.putExtra(Intent.EXTRA_TEXT, mToolbar.getTitle() + \" \" +  webViewS.getUrl() );\n                sendIntent.putExtra(Intent.EXTRA_TEXT, mToolbar.getTitle() + \" \" + agentWeb.getWebCreator().getWebView().getUrl());\n                sendIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n                startActivity(Intent.createChooser(sendIntent, getString(R.string.share_to)));\n                overridePendingTransition(R.anim.fade_in, R.anim.out_from_bottom);\n                break;\n            case R.id.web_menu_refresh:\n                agentWeb.getWebCreator().getWebView().reload();\n                break;\n            case R.id.web_menu_stop:\n                agentWeb.getWebCreator().getWebView().stopLoading();\n                break;\n            default:\n                break;\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n\n    /**\n     * Android旋转屏幕不销毁Activity\n     */\n    @Override\n    public void onConfigurationChanged(Configuration newConfig) {\n        super.onConfigurationChanged(newConfig);\n    }\n\n\n    private void exit() {\n        this.finish();\n        overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);\n    }\n\n    @Override\n    protected void onPause() {\n        super.onPause();\n        agentWeb.getWebLifeCycle().onPause();\n    }\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n        agentWeb.getWebLifeCycle().onResume();\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        CookieManager.getInstance().flush();\n        agentWeb.getWebCreator().getWebView().stopLoading();\n        agentWeb.getWebCreator().getWebView().clearCache(true);\n        agentWeb.getWebCreator().getWebView().clearHistory();\n        agentWeb.getWebCreator().getWebView().getSettings().setJavaScriptEnabled(false);\n        agentWeb.getWebCreator().getWebView().removeAllViews();\n        agentWeb.getWebCreator().getWebParentLayout().removeAllViews();\n        agentWeb.getWebLifeCycle().onDestroy();\n    }\n\n    @Override\n    protected Colorful.Builder buildColorful(Colorful.Builder mColorfulBuilder) {\n        mColorfulBuilder\n                .backgroundColor(R.id.web_root, R.attr.root_view_bg)\n                .backgroundColor(R.id.web_toolbar, R.attr.topbar_bg);\n        return mColorfulBuilder;\n    }\n\n\n    /**\n     *\n     作者：AmatorLee\n     链接：https://www.jianshu.com/p/b008e04987e0\n     來源：简书\n     简书著作权归作者所有，任何形式的转载都请联系作者获得授权并注明出处。\n\n     #####二、仿魅族应用商店应用详情效果\n     作为一个多年的魅族手机使用者，看起来魅族的应用商店也挺不错的，来看看要实现的效果（**注：实现效果而非实现实现界面**）\n\n     ![sheet](http://upload-images.jianshu.io/upload_images/2605454-3b9fc78ca7aaaa87.gif?imageMogr2/auto-orient/strip)\n\n     1. 思路一：使用Activity实现，但是这样需要解决的问题有：\n     1.  Activity进场/出场动画\n     2.     对于滑动的监听\n     3. 对状态栏的动态改变\n\n     2. 思路二：由于我们这边使用的是Behaviour，而系统给我们提供了一个```BottomSheetBehavior```应该可以完美的给我们解决滑动的问题，但是Activity方面的问题依然存在，然后找到了一个强大的Dialog(```BottomSheetDialog```)和一个DialogFragment(```BottomSheetDialogFragment```),,以我夜观天象应该这个就是实现了``````BottomSheetBehavior```的View，很好很强大。\n     我们来看看我们是怎样是实现的：\n\n     作者：AmatorLee\n     链接：https://www.jianshu.com/p/b008e04987e0\n     來源：简书\n     简书著作权归作者所有，任何形式的转载都请联系作者获得授权并注明出处。\n     */\n//    private void test(){\n//\n//        /**\n//         * BottomSheetDialog\n//         */\n//        Button btnShowDialog = (Button) findViewById(R.id.bottom_pull_sheet);\n//        mDatas = new ArrayList<>();\n//        View inflate = getLayoutInflater().inflate(R.layout.dialog_bottom_sheet, null);\n////        mLeftIcon = inflate.findViewById(R.id.delete);\n////        mLeftIcon.setOnClickListener(new View.OnClickListener() {\n////            @Override\n////            public void onClick(View view) {\n////                if (mBottomSheetDialog != null && mBottomSheetDialog.isShowing()) {\n////                    mBottomSheetDialog.dismiss();\n////                }\n////            }\n////        });\n////        RecyclerView recyclerView = inflate.findViewById(R.id.recyclerView);\n////        mDatas = new ArrayList<>();\n////        for (int i = 0; i < 50; i++) {\n////            mDatas.add(\"这是第\" + i + \"个数据\");\n////        }\n////        recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));\n////        recyclerView.setLayoutManager(new LinearLayoutManager(this));\n////        Adapter adapter = new Adapter();\n////        recyclerView.setAdapter(adapter);\n//\n//        BottomSheetDialog mBottomSheetDialog = new BottomSheetDialog(this);\n//        mBottomSheetDialog.setContentView(inflate);\n//        View container = mBottomSheetDialog.getDelegate().findViewById(android.support.design.R.id.design_bottom_sheet);\n//        final BottomSheetBehavior containerBehaviour = BottomSheetBehavior.from(container);\n//        containerBehaviour.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {\n//            @Override\n//            public void onStateChanged(@NonNull View bottomSheet, int newState) {\n//                KLog.e(TAG, \"onStateChanged: newState === \" + newState);\n//                if (newState == BottomSheetBehavior.STATE_HIDDEN) {\n//                    mBottomSheetDialog.dismiss();\n//                    containerBehaviour.setState(BottomSheetBehavior.STATE_COLLAPSED);\n//                } else if (newState == BottomSheetBehavior.STATE_COLLAPSED) {\n//                    //强制修改弹出高度为屏幕高度的0.9倍，不做此操作仅仅有CollapSed/Expand两种，就是0.5和1倍展开的效果\n//                    containerBehaviour.setPeekHeight((int) (0.9 * height));\n//                }\n//            }\n//\n//            @Override\n//            public void onSlide(@NonNull View bottomSheet, float slideOffset) {\n//                KLog.d(\"BottomBahaviour\", \"onSlide: slideOffset====\" + slideOffset);\n//                if (slideOffset == 1.0) {\n////                    mLeftIcon.setImageResource(R.drawable.back);\n//                    // containerBehaviour.setPeekHeight(height + getStatusBarHeight());\n//                    //修改状态栏\n//                    mBottomSheetDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);\n//                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n//                        mBottomSheetDialog.getWindow().setStatusBarColor(getResources().getColor(android.R.color.transparent));\n//                        try {\n//                            //修改魅族系统状态栏字体颜色\n//                            WindowManager.LayoutParams lp = mBottomSheetDialog.getWindow().getAttributes();\n//                            Field darkFlag = WindowManager.LayoutParams.class\n//                                    .getDeclaredField(\"MEIZU_FLAG_DARK_STATUS_BAR_ICON\");\n//                            Field meizuFlags = WindowManager.LayoutParams.class\n//                                    .getDeclaredField(\"meizuFlags\");\n//                            darkFlag.setAccessible(true);\n//                            meizuFlags.setAccessible(true);\n//                            int bit = darkFlag.getInt(null);\n//                            int value = meizuFlags.getInt(lp);\n//                            value |= bit;\n//                            meizuFlags.setInt(lp, value);\n//                            mBottomSheetDialog.getWindow().setAttributes(lp);\n//\n//                        } catch (Exception e) {\n//\n//                        }\n//                    }\n//                } else {\n//\n//                    mLeftIcon.setImageResource(R.drawable.icon_delete);\n//                }\n//            }\n//        });\n//\n//        btnShowDialog.setOnClickListener(new View.OnClickListener() {\n//            @Override\n//            public void onClick(View view) {\n//                if (!mBottomSheetDialog.isShowing()) {\n//                    containerBehaviour.setPeekHeight((int) (0.9 * height));\n//                    mBottomSheetDialog.portrait();\n//                } else {\n//                    mBottomSheetDialog.dismiss();\n//                }\n//            }\n//        });\n//    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/login/LoginFormState.java",
    "content": "package me.wizos.loread.activity.login;\n\nimport androidx.annotation.Nullable;\n\n/**\n * Data validation state of the login form.\n */\npublic class LoginFormState {\n    @Nullable\n    private Integer hostHint;\n    @Nullable\n    private Integer usernameHint;\n    @Nullable\n    private Integer passwordHint;\n    private boolean isDataValid;\n\n    public LoginFormState() { }\n\n    public LoginFormState(@Nullable Integer usernameHint, @Nullable Integer passwordHint) {\n        this.usernameHint = usernameHint;\n        this.passwordHint = passwordHint;\n        this.isDataValid = false;\n    }\n\n    public LoginFormState(boolean isDataValid) {\n        this.hostHint = null;\n        this.usernameHint = null;\n        this.passwordHint = null;\n        this.isDataValid = isDataValid;\n    }\n\n\n    public void setHostHint(@Nullable Integer hostHint) {\n        this.hostHint = hostHint;\n    }\n\n    public void setUsernameHint(@Nullable Integer usernameHint) {\n        this.usernameHint = usernameHint;\n    }\n\n    public void setPasswordHint(@Nullable Integer passwordHint) {\n        this.passwordHint = passwordHint;\n    }\n\n    @Nullable\n    Integer getHostHint() {\n        return hostHint;\n    }\n    @Nullable\n    Integer getUsernameHint() {\n        return usernameHint;\n    }\n\n    @Nullable\n    Integer getPasswordHint() {\n        return passwordHint;\n    }\n\n    public void setDataValid(boolean dataValid) {\n        isDataValid = dataValid;\n    }\n\n    boolean isDataValid() {\n        return isDataValid;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/login/LoginInoReaderActivity.java",
    "content": "package me.wizos.loread.activity.login;\n\nimport android.os.Bundle;\nimport android.text.Editable;\nimport android.text.TextWatcher;\nimport android.view.KeyEvent;\nimport android.view.View;\nimport android.view.inputmethod.EditorInfo;\nimport android.widget.Button;\nimport android.widget.EditText;\nimport android.widget.ProgressBar;\nimport android.widget.TextView;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.widget.Toolbar;\nimport androidx.lifecycle.Observer;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport com.hjq.toast.ToastUtils;\nimport com.socks.library.KLog;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.activity.BaseActivity;\nimport me.wizos.loread.view.colorful.Colorful;\nimport me.wizos.loread.viewmodel.InoReaderUserViewModel;\n\npublic class LoginInoReaderActivity extends BaseActivity {\n    private InoReaderUserViewModel loginViewModel;\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_login_inoreader);\n        Toolbar mToolbar = findViewById(R.id.inoreader_toolbar);\n        setSupportActionBar(mToolbar);\n        // 这个小于4.0版本是默认为true，在4.0及其以上是false。该方法的作用：决定左上角的图标是否可以点击(没有向左的小图标)，true 可点\n        getSupportActionBar().setHomeButtonEnabled(true);\n        // 决定左上角图标的左侧是否有向左的小箭头，true 有小箭头\n        getSupportActionBar().setDisplayHomeAsUpEnabled(true);\n        getSupportActionBar().setDisplayShowTitleEnabled(true);\n\n        loginViewModel = new ViewModelProvider(this).get(InoReaderUserViewModel.class);\n\n        final EditText hostEditText = findViewById(R.id.inoreader_host_edittext);\n        final EditText usernameEditText = findViewById(R.id.inoreader_username_edittext);\n        final EditText passwordEditText = findViewById(R.id.inoreader_password_edittext);\n        final Button loginButton = findViewById(R.id.inoreader_login_button);\n        final ProgressBar loadingProgressBar = findViewById(R.id.inoreader_loading);\n\n\n        TextWatcher afterTextChangedListener = new TextWatcher() {\n            @Override\n            public void beforeTextChanged(CharSequence s, int start, int count, int after) {\n            }\n\n            @Override\n            public void onTextChanged(CharSequence s, int start, int before, int count) {\n            }\n\n            @Override\n            public void afterTextChanged(Editable s) {\n                loginViewModel.loginDataChanged(\n                        hostEditText.getText().toString(),\n                        usernameEditText.getText().toString(),\n                        passwordEditText.getText().toString()\n                );\n            }\n        };\n        hostEditText.addTextChangedListener(afterTextChangedListener);\n        usernameEditText.addTextChangedListener(afterTextChangedListener);\n        passwordEditText.addTextChangedListener(afterTextChangedListener);\n\n        /**\n         * 需要注意的是 setOnEditorActionListener这个方法，并不是在我们点击EditText的时候触发，\n         * 也不是在我们对EditText进行编辑时触发，而是在我们编辑完之后点击软键盘上的各种键才会触发。\n         * 因为通过布局文件中的imeOptions可以控制软件盘右下角的按钮显示为不同按钮。所以和EditorInfo搭配起来可以实现各种软键盘的功能。\n         */\n        passwordEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {\n            @Override\n            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {\n                if (actionId == EditorInfo.IME_ACTION_DONE) {\n                    loadingProgressBar.setVisibility(View.VISIBLE);\n                    loginViewModel.login(\n                            hostEditText.getText().toString(),\n                            usernameEditText.getText().toString(),\n                            passwordEditText.getText().toString()\n                    );\n                }\n                return false;\n            }\n        });\n\n        loginButton.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                loadingProgressBar.setVisibility(View.VISIBLE);\n                loginButton.setEnabled(false);\n                loginViewModel.login(\n                        hostEditText.getText().toString(),\n                        usernameEditText.getText().toString(),\n                        passwordEditText.getText().toString()\n                );\n            }\n        });\n\n        loginViewModel.getLoginFormLiveData().observe(this, new Observer<LoginFormState>() {\n            @Override\n            public void onChanged(@Nullable LoginFormState loginFormState) {\n                if (loginFormState == null) {\n                    return;\n                }\n                loginButton.setEnabled(loginFormState.isDataValid());\n\n                if (loginFormState.getHostHint() != null) {\n                    hostEditText.setError(getString(loginFormState.getHostHint()));\n                }\n                if (loginFormState.getUsernameHint() != null) {\n                    usernameEditText.setError(getString(loginFormState.getUsernameHint()));\n                }\n                if (loginFormState.getPasswordHint() != null) {\n                    passwordEditText.setError(getString(loginFormState.getPasswordHint()));\n                }\n            }\n        });\n\n        loginViewModel.getLoginResult().observe(this, new Observer<LoginResult>() {\n            @Override\n            public void onChanged(@Nullable LoginResult loginResult) {\n                if (loginResult == null) {\n                    return;\n                }\n                loginButton.setEnabled(true);\n                loadingProgressBar.setVisibility(View.GONE);\n                String tips = getString(R.string.welcome);\n                if (loginResult.isSuccess()) {\n                    ToastUtils.show(tips);\n                    KLog.e( tips + loginResult.getData() );\n                    setResult(App.ActivityResult_LoginPageToProvider);\n                    finish();\n                    overridePendingTransition(R.anim.fade_in, R.anim.out_from_bottom);\n                } else {\n                    ToastUtils.show(loginResult.getData());\n                }\n            }\n        });\n    }\n    public Colorful.Builder buildColorful(Colorful.Builder mColorfulBuilder) {\n        return mColorfulBuilder;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/login/LoginResult.java",
    "content": "package me.wizos.loread.activity.login;\n\nimport androidx.annotation.Nullable;\n\n/**\n * Authentication result : success (user details) or error message.\n */\npublic class LoginResult {\n    @Nullable\n    private boolean success;\n    @Nullable\n    private String data;\n\n    public LoginResult() {\n    }\n\n    @Nullable\n    public LoginResult setSuccess(boolean success) {\n        this.success = success;\n        return this;\n    }\n\n    @Nullable\n    public boolean isSuccess() {\n        return success;\n    }\n\n    @Nullable\n    public String getData() {\n        return data;\n    }\n\n    public LoginResult setData(@Nullable String data) {\n        this.data = data;\n        return this;\n    }\n\n    @Override\n    public String toString() {\n        return \"LoginResult{\" +\n                \"success=\" + success +\n                \", data='\" + data + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/activity/login/LoginTinyRSSActivity.java",
    "content": "package me.wizos.loread.activity.login;\n\nimport android.os.Bundle;\nimport android.text.Editable;\nimport android.text.TextWatcher;\nimport android.view.KeyEvent;\nimport android.view.View;\nimport android.view.inputmethod.EditorInfo;\nimport android.widget.Button;\nimport android.widget.EditText;\nimport android.widget.ProgressBar;\nimport android.widget.TextView;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.widget.Toolbar;\nimport androidx.lifecycle.Observer;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport com.hjq.toast.ToastUtils;\nimport com.socks.library.KLog;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.activity.BaseActivity;\nimport me.wizos.loread.view.colorful.Colorful;\nimport me.wizos.loread.viewmodel.TinyRSSUserViewModel;\n\npublic class LoginTinyRSSActivity extends BaseActivity {\n    private TinyRSSUserViewModel loginViewModel;\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_login_tiny_rss);\n        Toolbar mToolbar = findViewById(R.id.tiny_rss_toolbar);\n        setSupportActionBar(mToolbar);\n        // 这个小于4.0版本是默认为true，在4.0及其以上是false。该方法的作用：决定左上角的图标是否可以点击(没有向左的小图标)，true 可点\n        getSupportActionBar().setHomeButtonEnabled(true);\n        // 决定左上角图标的左侧是否有向左的小箭头，true 有小箭头\n        getSupportActionBar().setDisplayHomeAsUpEnabled(true);\n        getSupportActionBar().setDisplayShowTitleEnabled(true);\n\n        loginViewModel = new ViewModelProvider(this).get(TinyRSSUserViewModel.class);\n\n        final EditText hostEditText = findViewById(R.id.tiny_rss_host_edittext);\n        final EditText usernameEditText = findViewById(R.id.tiny_rss_username_edittext);\n        final EditText passwordEditText = findViewById(R.id.tiny_rss_password_edittext);\n        final Button loginButton = findViewById(R.id.tiny_rss_login_button);\n        final ProgressBar loadingProgressBar = findViewById(R.id.tiny_rss_loading);\n\n\n        TextWatcher afterTextChangedListener = new TextWatcher() {\n            @Override\n            public void beforeTextChanged(CharSequence s, int start, int count, int after) {\n            }\n\n            @Override\n            public void onTextChanged(CharSequence s, int start, int before, int count) {\n            }\n\n            @Override\n            public void afterTextChanged(Editable s) {\n                loginViewModel.loginDataChanged(\n                        hostEditText.getText().toString(),\n                        usernameEditText.getText().toString(),\n                        passwordEditText.getText().toString()\n                );\n            }\n        };\n        hostEditText.addTextChangedListener(afterTextChangedListener);\n        usernameEditText.addTextChangedListener(afterTextChangedListener);\n        passwordEditText.addTextChangedListener(afterTextChangedListener);\n\n        /**\n         * 需要注意的是 setOnEditorActionListener这个方法，并不是在我们点击EditText的时候触发，\n         * 也不是在我们对EditText进行编辑时触发，而是在我们编辑完之后点击软键盘上的各种键才会触发。\n         * 因为通过布局文件中的imeOptions可以控制软件盘右下角的按钮显示为不同按钮。所以和EditorInfo搭配起来可以实现各种软键盘的功能。\n         */\n        passwordEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {\n            @Override\n            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {\n                if (actionId == EditorInfo.IME_ACTION_DONE) {\n                    loadingProgressBar.setVisibility(View.VISIBLE);\n                    loginViewModel.login(\n                            hostEditText.getText().toString(),\n                            usernameEditText.getText().toString(),\n                            passwordEditText.getText().toString()\n                    );\n                }\n                return false;\n            }\n        });\n\n        loginButton.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                loadingProgressBar.setVisibility(View.VISIBLE);\n                loginButton.setEnabled(false);\n                loginViewModel.login(\n                        hostEditText.getText().toString(),\n                        usernameEditText.getText().toString(),\n                        passwordEditText.getText().toString()\n                );\n            }\n        });\n\n        loginViewModel.getLoginFormLiveData().observe(this, new Observer<LoginFormState>() {\n            @Override\n            public void onChanged(@Nullable LoginFormState loginFormState) {\n                if (loginFormState == null) {\n                    return;\n                }\n                loginButton.setEnabled(loginFormState.isDataValid());\n\n                if (loginFormState.getHostHint() != null) {\n                    hostEditText.setError(getString(loginFormState.getHostHint()));\n                }\n                if (loginFormState.getUsernameHint() != null) {\n                    usernameEditText.setError(getString(loginFormState.getUsernameHint()));\n                }\n                if (loginFormState.getPasswordHint() != null) {\n                    passwordEditText.setError(getString(loginFormState.getPasswordHint()));\n                }\n            }\n        });\n\n        loginViewModel.getLoginResult().observe(this, new Observer<LoginResult>() {\n            @Override\n            public void onChanged(@Nullable LoginResult loginResult) {\n                if (loginResult == null) {\n                    return;\n                }\n                loginButton.setEnabled(true);\n                loadingProgressBar.setVisibility(View.GONE);\n                String tips = getString(R.string.welcome);\n                if (loginResult.isSuccess()) {\n                    ToastUtils.show(tips);\n                    KLog.e( tips + loginResult.getData() );\n                    setResult(App.ActivityResult_LoginPageToProvider);\n                    finish();\n                    overridePendingTransition(R.anim.fade_in, R.anim.out_from_bottom);\n                } else {\n                    ToastUtils.show(loginResult.getData());\n                }\n            }\n        });\n    }\n    public Colorful.Builder buildColorful(Colorful.Builder mColorfulBuilder) {\n        return mColorfulBuilder;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/adapter/ArticlePagedListAdapter.java",
    "content": "package me.wizos.loread.adapter;\n\nimport android.content.Context;\nimport android.text.Html;\nimport android.text.TextUtils;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.paging.PagedListAdapter;\nimport androidx.recyclerview.widget.DiffUtil;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.bumptech.glide.Glide;\nimport com.bumptech.glide.Priority;\nimport com.bumptech.glide.load.model.GlideUrl;\nimport com.bumptech.glide.load.model.LazyHeaders;\nimport com.bumptech.glide.request.RequestOptions;\nimport com.carlt.networklibs.NetType;\nimport com.carlt.networklibs.utils.NetworkUtils;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.Feed;\nimport me.wizos.loread.utils.TimeUtil;\nimport me.wizos.loread.view.IconFontView;\n\npublic class ArticlePagedListAdapter extends PagedListAdapter<Article, ArticlePagedListAdapter.ArticleViewHolder> {\n    private RequestOptions canDownloadOptions;\n    private RequestOptions cannotDownloadOptions;\n    private Context context;\n\n    public ArticlePagedListAdapter() {\n        super(DIFF_CALLBACK);\n    }\n\n    @NonNull\n    public ArticleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) {\n        context = parent.getContext();\n        canDownloadOptions = new RequestOptions()\n                .centerCrop()\n                .onlyRetrieveFromCache(false)\n                .priority(Priority.HIGH);\n        cannotDownloadOptions = new RequestOptions()\n                .centerCrop()\n                .onlyRetrieveFromCache(true)\n                .priority(Priority.HIGH);\n        return new ArticleViewHolder(LayoutInflater.from(context).inflate(R.layout.activity_main_list_item, parent, false));\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull ArticleViewHolder holder, int position) {\n        Article article = getItem(position);\n        if (article != null) {\n            holder.bindTo(article);\n        }\n        else {\n            // Null defines a placeholder item - PagedListAdapter automatically invalidates this row when the actual object is loaded from the database.\n            holder.placeholder();\n        }\n    }\n\n    private static DiffUtil.ItemCallback<Article> DIFF_CALLBACK = new DiffUtil.ItemCallback<Article>() {\n        @Override\n        public boolean areItemsTheSame(Article oldArticle, Article newArticle) {\n            return oldArticle.getId().equals(newArticle.getId());\n        }\n\n        @Override\n        public boolean areContentsTheSame(Article oldArticle, Article newArticle) {\n            return oldArticle.getReadStatus() == newArticle.getReadStatus()\n                    && oldArticle.getStarStatus() == newArticle.getStarStatus()\n                    && oldArticle.getSaveStatus() == newArticle.getSaveStatus()\n                    && oldArticle.getTitle().equals(newArticle.getTitle())\n                    && (oldArticle.getImage() != null && oldArticle.getImage().equals(newArticle.getImage()) )\n                    && oldArticle.getSummary().equals(newArticle.getSummary());\n        }\n    };\n\n\n    class ArticleViewHolder extends RecyclerView.ViewHolder {\n        @NonNull\n        TextView articleTitle;\n        TextView articleSummary;\n        TextView articleFeed;\n        TextView articlePublished;\n        IconFontView articleStar;\n        IconFontView articleReading;\n        IconFontView articleSave;\n        ImageView articleImg;\n\n        ArticleViewHolder(@NonNull View itemView) {\n            super(itemView);\n            articleTitle = (TextView) itemView.findViewById(R.id.main_slv_item_title);\n            articleSummary = (TextView) itemView.findViewById(R.id.main_slv_item_summary);\n            articleFeed = (TextView) itemView.findViewById(R.id.main_slv_item_author);\n            articleImg = (ImageView) itemView.findViewById(R.id.main_slv_item_img);\n            articlePublished = (TextView) itemView.findViewById(R.id.main_slv_item_time);\n            articleStar = (IconFontView) itemView.findViewById(R.id.main_slv_item_icon_star);\n            articleReading = (IconFontView) itemView.findViewById(R.id.main_slv_item_icon_reading);\n            articleSave = (IconFontView) itemView.findViewById(R.id.main_slv_item_icon_save);\n        }\n        void placeholder(){\n            articleTitle.setText(App.i().getString(R.string.loading));\n            articleTitle.setAlpha(0.40f);\n\n            articleSummary.setText(\"\");\n//            articleSummary.setVisibility(View.GONE);\n            articleImg.setVisibility(View.GONE);\n\n            articleSummary.setText(\"\");\n//            articleFeed.setVisibility(View.GONE);\n//            articleFeed.setText(App.i().getString(R.string.loading));\n//            articlePublished.setText(\"\");\n            articleSave.setVisibility(View.GONE);\n            articleReading.setVisibility(View.GONE);\n            articleStar.setVisibility(View.GONE);\n        }\n\n\n        void bindTo(Article article){\n            if (TextUtils.isEmpty(article.getTitle())) {\n                articleTitle.setText(App.i().getString(R.string.no_title));\n            } else {\n                articleTitle.setText(article.getTitle());\n            }\n            if (article.getReadStatus() == App.STATUS_READED) {\n                articleTitle.setAlpha(0.40f);\n            } else {\n                articleTitle.setAlpha(1f);\n            }\n\n            if (TextUtils.isEmpty(article.getSummary()) || article.getSummary().length() == 0) {\n                articleSummary.setVisibility(View.GONE);\n            } else {\n                articleSummary.setVisibility(View.VISIBLE);\n                articleSummary.setText(article.getSummary());\n            }\n\n            if (!TextUtils.isEmpty(article.getImage())) {\n                articleImg.setVisibility(View.VISIBLE);\n\n                if ( NetworkUtils.isAvailable() && (!App.i().getUser().isDownloadImgOnlyWifi() || NetworkUtils.getNetType().equals(NetType.WIFI)) ) {\n                    // KLog.e( \"数据：\" + article.getTitle() + \"   \"  +  App.Referer+ \"   \"  +  article.getLink() );\n                    if (!TextUtils.isEmpty(article.getLink())) {\n                        GlideUrl gliderUrl = new GlideUrl(article.getImage(), new LazyHeaders.Builder().addHeader(App.Referer, article.getLink()).build());\n                        Glide.with(context).load(gliderUrl).apply(canDownloadOptions).into(articleImg);\n                    } else {\n                        Glide.with(context).load(article.getImage()).apply(canDownloadOptions).into(articleImg);\n                    }\n                } else {\n                    Glide.with(context).load(article.getImage()).apply(cannotDownloadOptions).into(articleImg);\n                }\n            } else {\n                articleImg.setVisibility(View.GONE);\n            }\n\n            Feed feed = CoreDB.i().feedDao().getById(App.i().getUser().getId(),article.getFeedId());\n            if (feed != null && !TextUtils.isEmpty(feed.getTitle())) {\n                articleFeed.setText(Html.fromHtml(feed.getTitle()));\n            } else {\n                articleFeed.setText(article.getFeedTitle());\n            }\n\n            articlePublished.setText(TimeUtil.format(article.getPubDate(), \"yyyy-MM-dd HH:mm\"));\n\n            if (App.STATUS_NOT_FILED == article.getSaveStatus()) {\n                articleSave.setVisibility(View.GONE);\n            } else {\n                articleSave.setVisibility(View.VISIBLE);\n            }\n\n            if (article.getReadStatus() == App.STATUS_UNREADING) {\n                articleReading.setVisibility(View.VISIBLE);\n            } else {\n                articleReading.setVisibility(View.GONE);\n            }\n            if (article.getStarStatus() == App.STATUS_STARED) {\n                articleStar.setVisibility(View.VISIBLE);\n            } else {\n                articleStar.setVisibility(View.GONE);\n            }\n        }\n    }\n\n    /**\n     * 之所以会产生“更新页面最后几项而下一页前几项会跳动”，是因为：\n     * 更新页面最后几项时，使用了getItem来获取，而在getItem的默认实现中，会将getItem不为null标识为PagedList的LastKey（需要加载的最后一项）。\n     * 但是实际上被修改的项不是视图中的最后一项，所以视图中下一页的前几项会需要重新加载，进而走到onBindViewHolder的getItem。\n     * 又因为这几项没有提前被加载到内存中，所以得到的是null，又触发了更新为占位符的逻辑，等到数据加载完了重新渲染时，就产生了跳动的现象。\n     */\n    private int lastPos = 0;\n    @Override\n    public Article getItem(int position) {\n//        return super.getItem(position);\n        Article article = super.getItem(position);\n        if(position < lastPos && lastPos < getItemCount() ){\n            super.getItem(lastPos);\n        }else {\n            lastPos = position;\n        }\n        //KLog.e(\"加载：\" + position + \" == \" + lastPos  + \" , \" + getCurrentList().getLastKey()  + \" == \"+ getCurrentList().getLoadedCount() + \" -- \" + (article==null));\n        return article;\n    }\n    public void setLastItem(int position){\n        lastPos = position;\n        super.getItem(position);\n    }\n    public void setLastPos(int position){\n        lastPos = position;\n    }\n//    public void resetLastItem(int position){\n//        if( getItemCount() == 0){\n//            return;\n//        }\n//        if( position < getItemCount() && position >= 0){\n//            super.getItem(position);\n//        }\n//    }\n    public void load(int index){\n        if(getCurrentList() !=null && index < getCurrentList().size()){\n            getCurrentList().loadAround(index);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/adapter/ArticleViewBinder.java",
    "content": "//package me.wizos.loread.adapter;\n//\n//import android.content.Context;\n//import android.text.Html;\n//import android.text.TextUtils;\n//import android.view.LayoutInflater;\n//import android.view.View;\n//import android.view.ViewGroup;\n//import android.widget.ImageView;\n//import android.widget.TextView;\n//\n//import androidx.annotation.NonNull;\n//import androidx.recyclerview.widget.RecyclerView;\n//\n//import com.bumptech.glide.Glide;\n//import com.bumptech.glide.Priority;\n//import com.bumptech.glide.load.model.GlideUrl;\n//import com.bumptech.glide.load.model.LazyHeaders;\n//import com.bumptech.glide.request.RequestOptions;\n//import com.carlt.networklibs.NetType;\n//import com.carlt.networklibs.utils.NetworkUtils;\n//import com.drakeet.multitype.ItemViewBinder;\n//\n//import me.wizos.loread.App;\n//import me.wizos.loread.R;\n//import me.wizos.loread.db.Article;\n//import me.wizos.loread.db.CoreDB;\n//import me.wizos.loread.db.Feed;\n//import me.wizos.loread.utils.TimeUtil;\n//import me.wizos.loread.view.IconFontView;\n//\n///**\n// * Created by Wizos on 2019/4/14.\n// */\n//\n//public class ArticleViewBinder extends ItemViewBinder<Article, ArticleViewBinder.ArticleViewHolder> {\n//    private RequestOptions canDownloadOptions;\n//    private RequestOptions cannotDownloadOptions;\n//    private GlideUrl gliderUrl;\n//    private Context context;\n//\n//    @NonNull\n//    @Override\n//    public ArticleViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {\n//        context = parent.getContext();\n//        canDownloadOptions = new RequestOptions()\n//                .centerCrop()\n//                .onlyRetrieveFromCache(false)\n//                .priority(Priority.HIGH);\n//        cannotDownloadOptions = new RequestOptions()\n//                .centerCrop()\n//                .onlyRetrieveFromCache(true)\n//                .priority(Priority.HIGH);\n//        return new ArticleViewHolder(inflater.inflate(R.layout.activity_main_list_item, parent, false));\n//    }\n//\n//\n//    @Override\n//    public void onBindViewHolder(@NonNull ArticleViewHolder holder, @NonNull Article article) {\n//        if (TextUtils.isEmpty(article.getTitle())) {\n//            holder.articleTitle.setText(App.i().getString(R.string.no_title));\n//        } else {\n//            holder.articleTitle.setText(article.getTitle());\n//        }\n//\n//        if (TextUtils.isEmpty(article.getSummary()) || article.getSummary().length() == 0) {\n//            holder.articleSummary.setVisibility(View.GONE);\n//        } else {\n//            holder.articleSummary.setVisibility(View.VISIBLE);\n//            holder.articleSummary.setText(article.getSummary());\n//        }\n//\n//        if (!TextUtils.isEmpty(article.getImage())) {\n//            holder.articleImg.setVisibility(View.VISIBLE);\n//\n//\n//            if ( NetworkUtils.isAvailable() && (!App.i().getUser().isDownloadImgOnlyWifi() || NetworkUtils.getNetType().equals(NetType.WIFI)) ) {\n//                // KLog.e( \"数据：\" + article.getTitle() + \"   \"  +  App.Referer+ \"   \"  +  article.getLink() );\n//                if (!TextUtils.isEmpty(article.getLink())) {\n//                    gliderUrl = new GlideUrl(article.getImage(), new LazyHeaders.Builder().addHeader(App.Referer, article.getLink()).build());\n//                    Glide.with(context).load(gliderUrl).apply(canDownloadOptions).into(holder.articleImg);\n//                } else {\n//                    Glide.with(context).load(article.getImage()).apply(canDownloadOptions).into(holder.articleImg);\n//                }\n//            } else {\n//                Glide.with(context).load(article.getImage()).apply(cannotDownloadOptions).into(holder.articleImg);\n//            }\n//        } else {\n//            holder.articleImg.setVisibility(View.GONE);\n//        }\n//\n//        Feed feed = CoreDB.i().feedDao().getById(App.i().getUser().getId(), article.getFeedId());\n//        if (feed != null && !TextUtils.isEmpty(feed.getTitle())) {\n//            holder.articleFeed.setText(Html.fromHtml(feed.getTitle()));\n//        } else {\n//            holder.articleFeed.setText(article.getFeedTitle());\n//        }\n//\n//        holder.articlePublished.setText(TimeUtil.format(article.getPubDate(), \"yyyy-MM-dd HH:mm\"));\n//\n//        if (article.getReadStatus() == App.STATUS_READED) {\n//            holder.articleTitle.setAlpha(0.40f);\n//        } else {\n//            holder.articleTitle.setAlpha(1f);\n//        }\n//\n//        if (App.STATUS_NOT_FILED == article.getSaveStatus()) {\n//            holder.articleSave.setVisibility(View.GONE);\n//        } else {\n//            holder.articleSave.setVisibility(View.VISIBLE);\n//        }\n//\n//        if (article.getReadStatus() == App.STATUS_UNREADING) {\n//            holder.articleReading.setVisibility(View.VISIBLE);\n//        } else if (article.getReadStatus() == App.STATUS_UNREAD) {\n//            holder.articleReading.setVisibility(View.GONE);\n//        } else {\n//            holder.articleReading.setVisibility(View.GONE);\n//        }\n//        if (article.getStarStatus() == App.STATUS_STARED) {\n//            holder.articleStar.setVisibility(View.VISIBLE);\n//        } else {\n//            holder.articleStar.setVisibility(View.GONE);\n//        }\n//\n//    }\n//\n//    static class ArticleViewHolder extends RecyclerView.ViewHolder {\n//        @NonNull\n//        TextView articleTitle;\n//        TextView articleSummary;\n//        TextView articleFeed;\n//        TextView articlePublished;\n//        IconFontView articleStar;\n//        IconFontView articleReading;\n//        IconFontView articleSave;\n//        ImageView articleImg;\n////        IconFontView markLeft;\n////        IconFontView markRight;\n////        SwipeDragLayout swipeDragLayout;\n//\n//        ArticleViewHolder(@NonNull View itemView) {\n//            super(itemView);\n//            articleTitle = (TextView) itemView.findViewById(R.id.main_slv_item_title);\n//            articleSummary = (TextView) itemView.findViewById(R.id.main_slv_item_summary);\n//            articleFeed = (TextView) itemView.findViewById(R.id.main_slv_item_author);\n//            articleImg = (ImageView) itemView.findViewById(R.id.main_slv_item_img);\n//            // articleImg.setBackgroundColor(itemView.getContext().getResources().getColor(R.color.placeholder_bg));\n//            articlePublished = (TextView) itemView.findViewById(R.id.main_slv_item_time);\n//            articleStar = (IconFontView) itemView.findViewById(R.id.main_slv_item_icon_star);\n//            articleReading = (IconFontView) itemView.findViewById(R.id.main_slv_item_icon_reading);\n//            articleSave = (IconFontView) itemView.findViewById(R.id.main_slv_item_icon_save);\n////            markLeft = itemView.findViewById(R.id.main_list_item_menu_left);\n////            markRight = itemView.findViewById(R.id.main_list_item_menu_right);\n////            swipeDragLayout = itemView.findViewById(R.id.swipe_layout);\n//        }\n//    }\n//\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/adapter/ExpandedAdapter.java",
    "content": "package me.wizos.loread.adapter;\n\nimport android.content.Context;\nimport android.util.ArrayMap;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.socks.library.KLog;\nimport com.yanzhenjie.recyclerview.ExpandableAdapter;\n\nimport java.util.List;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.db.Collection;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.view.IconFontView;\n\n/**\n * Created by Wizos on 2019/4/17.\n */\n\npublic class ExpandedAdapter extends ExpandableAdapter<RecyclerView.ViewHolder> {\n    private LayoutInflater mInflater;\n    private List<Collection> categories;\n    //private int countMode = App.STATUS_ALL; // 0为所有， 1为unread， 2为star\n    private ArrayMap<String, List<Collection>> feedsMap = new ArrayMap<>();\n\n    public ExpandedAdapter(Context context) {\n        this.mInflater = LayoutInflater.from(context);\n    }\n//    public ExpandedAdapter(Context context, int countMode) {\n//        this.mInflater = LayoutInflater.from(context);\n//        this.countMode = countMode;\n//    }\n\n    public void setParents(List<Collection> parents) {\n        this.categories = parents;\n    }\n    public List<Collection> getParents() {\n        return categories;\n    }\n\n\n    public void notifyDataChanged() {\n        //KLog.e(\"获得notifyDataSetChanged\");\n        feedsMap = new ArrayMap<>();\n        super.notifyDataSetChanged();\n    }\n\n\n    public Collection getGroup(int groupPos){\n        return categories.get(groupPos);\n    }\n\n    public Collection getChild(int groupPos, int childPos) {\n        return getChildren(groupPos).get(childPos);\n    }\n\n    private List<Collection> getChildren(int groupPos) {\n        //KLog.e(\"getFeeds\");\n        if (null == feedsMap.get(categories.get(groupPos).getId())) {\n            long time = System.currentTimeMillis();\n            List<Collection> feedWraps;\n\n            if( App.i().getUser().getStreamStatus() == App.STATUS_UNREAD ){\n                feedWraps = CoreDB.i().feedDao().getFeedsUnreadCountByCategoryId(App.i().getUser().getId(), categories.get(groupPos).getId());\n            }else if( App.i().getUser().getStreamStatus() == App.STATUS_STARED ){\n                feedWraps = CoreDB.i().feedDao().getFeedsStarCountByCategoryId(App.i().getUser().getId(), categories.get(groupPos).getId());\n            }else {\n                feedWraps = CoreDB.i().feedDao().getFeedsAllCountByCategoryId(App.i().getUser().getId(), categories.get(groupPos).getId());\n            }\n\n            long d = System.currentTimeMillis() - time;\n            KLog.e(\"返回feedList，耗时：\" + d + \" = \" + App.i().getUser().getId()  + \" = \" + categories.get(groupPos).getId() );\n            feedsMap.put(categories.get(groupPos).getId(), feedWraps);\n\n            return feedWraps;\n        }\n        return feedsMap.get(categories.get(groupPos).getId());\n    }\n\n    @Override\n    public int parentItemCount() {\n//        KLog.e(\"parentItemCount\");\n        return categories == null ? 0 : categories.size();\n    }\n\n    @Override\n    public int childItemCount(int parentPosition) {\n        //KLog.e(\"childItemCount\");\n        List<Collection> memberList = getChildren(parentPosition);\n        return memberList == null ? 0 : memberList.size();\n    }\n\n    @Override\n    public RecyclerView.ViewHolder createParentHolder(@NonNull ViewGroup root, int viewType) {\n//        KLog.e(\"createParentHolder\");\n        View view = mInflater.inflate(R.layout.tag_expandable_item_group, root, false);\n        return new ParentHolder(view);\n    }\n\n    @Override\n    public RecyclerView.ViewHolder createChildHolder(@NonNull ViewGroup root, int viewType) {\n        //KLog.e(\"createChildHolder\");\n        View view = mInflater.inflate(R.layout.tag_expandable_item_child, root, false);\n        return new ChildHolder(view);\n    }\n\n    @Override\n    public void bindParentHolder(@NonNull RecyclerView.ViewHolder holder, int position) {\n//        KLog.e(\"bindParentHolder\");\n        ((ParentHolder) holder).setData(this, categories.get(position), position);\n    }\n\n    @Override\n    public void bindChildHolder(@NonNull RecyclerView.ViewHolder holder, int parentPosition, int position) {\n        //KLog.e(\"bindChildHolder\");\n        ((ChildHolder) holder).setData(getChildren(parentPosition).get(position));\n    }\n\n    static class ParentHolder extends RecyclerView.ViewHolder {\n        Context context;\n        IconFontView icon;\n        TextView title;\n        TextView countView;\n        ExpandedAdapter adapter;\n\n        ParentHolder(@NonNull View itemView) {\n            super(itemView);\n            icon = itemView.findViewById(R.id.group_item_icon);\n            title = itemView.findViewById(R.id.group_item_title);\n            countView = itemView.findViewById(R.id.group_item_count);\n            context = itemView.getContext();\n        }\n\n        public void setData(@NonNull ExpandedAdapter mAdapter, @NonNull Collection category, @NonNull final int parentPosition) {\n//            KLog.e(\"设置父的数据A：\" + category);\n            adapter = mAdapter;\n//            if(!category.getId().contains(App.CATEGORY_ALL) && !category.getId().contains(App.CATEGORY_UNCATEGORIZED)){\n//                category = CoreDB.i().categoryDao().getById(App.i().getUser().getId(), category.getId());\n//            }\n\n            if (CoreDB.i().feedCategoryDao().getCountByCategoryId(App.i().getUser().getId(), category.getId()) == 0) {\n                icon.setText(context.getString(R.string.font_tag));\n            } else if (adapter.isExpanded(parentPosition)) {\n                icon.setText(context.getString(R.string.font_arrow_down));\n            } else {\n                icon.setText(context.getString(R.string.font_arrow_right));\n            }\n\n//            KLog.e(\"设置父的数据B \");\n            title.setText(category.getTitle());\n\n            int count = category.getCount();\n            if (count > 0) {\n                countView.setText(String.valueOf(count));\n                countView.setVisibility(View.VISIBLE);\n            } else {\n                countView.setVisibility(View.INVISIBLE);\n            }\n\n\n            icon.setOnClickListener(new View.OnClickListener() {\n                @Override\n                public void onClick(View v) {\n                    long time = System.currentTimeMillis();\n                    // 判断parent是否打开了二级菜单\n                    if (adapter.isExpanded(parentPosition)) {\n                        // 关闭该parent下的二级菜单\n                        adapter.collapseParent(parentPosition);\n                        icon.setText(context.getString(R.string.font_arrow_right));\n                    } else {\n                        // 打开该parent下的二级菜单\n                        adapter.expandParent(parentPosition);\n                        icon.setText(context.getString(R.string.font_arrow_down));\n                    }\n                    KLog.e(\"点击展开收缩：\" + (System.currentTimeMillis() - time) );\n                }\n            });\n        }\n    }\n\n    static class ChildHolder extends RecyclerView.ViewHolder {\n        TextView title;\n        TextView countView;\n\n        ChildHolder(@NonNull View itemView) {\n            super(itemView);\n            title = itemView.findViewById(R.id.child_item_title);\n            countView = itemView.findViewById(R.id.child_item_count);\n        }\n\n        public void setData(Collection feed) {\n            //feed.refresh();\n            title.setText(feed.getTitle());\n            int count = feed.getCount();\n            countView.setText(String.valueOf(count));\n            countView.setVisibility(count > 0 ? View.VISIBLE : View.INVISIBLE);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/Enclosure.java",
    "content": "package me.wizos.loread.bean;\n\nimport com.google.gson.annotations.SerializedName;\n\n/**\n * Created by Wizos on 2019/2/8.\n */\n\npublic class Enclosure {\n    @SerializedName(value = \"href\", alternate = {\"content_url\"})\n    private String href;\n    // 值有text/html、image/jpeg、application/rss+xml; charset=UTF-8（href是https://justyy.com/feed）\n    @SerializedName(value = \"type\", alternate = {\"content_type\"})\n    private String type;\n\n    public String getHref() {\n        return href;\n    }\n\n    public void setHref(String href) {\n        this.href = href;\n    }\n\n    public String getType() {\n        return type;\n    }\n\n    public void setType(String type) {\n        this.type = type;\n    }\n\n    public String toString() {\n        return \"Enclosure  [href:\" + href + \",  type:\" + type + \"]\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/LogLevel.java",
    "content": "//package me.wizos.loreadx.bean;\n//\n//public enum LogLevel {\n//    debug(\"debug\"),info(\"info\"),warning(\"warning\"),error(\"error\"),none(\"none\");\n//\n//    private String level;\n//\n//    LogLevel(String level) { }\n//\n//    public String getLevel() {\n//        return level;\n//    }\n//\n//    public void setLevel(String level) {\n//        this.level = level;\n//    }\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/Token.java",
    "content": "package me.wizos.loread.bean;\n\nimport android.text.TextUtils;\n\nimport com.google.gson.annotations.SerializedName;\nimport com.socks.library.KLog;\n\n/**\n * Created by Wizos on 2019/2/17.\n */\n\npublic class Token {\n    /*\n    1、refresh_token也有过期时间，如百度云平台会提供有效期为1个月的Access Token和有效期为10年的Refresh Token，\n    2、当refresh_token过期的时候，则需要用户重新授权登录。 \n    3、每次登录后，会产生新token，原来的access_token与refresh_token自然失效。\n    4、refresh_token 仅能使用一次，使用一次后，将被废弃。\n    原文：https://blog.csdn.net/lvxiangan/article/details/78020674\n     */\n    private String access_token;\n    private String refresh_token;\n    private String token_type;\n    // 秒\n    private long expires_in;\n\n    @SerializedName(\"error\")\n    private String error;\n    @SerializedName(\"auth\")\n    private String auth;\n\n\n//    {\n//        \"access_token\": \"[ACCESS_TOKEN]\",\n//            \"token_type\": \"Bearer\",\n//            \"expires_in\": [EXPIRATION_IN_SECONDS],\n//        \"refresh_token\": \"[REFRESH_TOKEN]\",\n//            \"scope\": \"read\"\n//    }\n//    {\n//        \"id\": \"c805fcbf-3acf-4302-a97e-d82f9d7c897f\",\n//            \"refresh_token\": \"AQAA7rJ7InAiOjEsImEiOiJmZWVk...\",\n//            \"access_token\": \"AQAAF4iTvPam_M4_dWheV_5NUL8E...\",\n//            \"expires_in\": 3920,\n//            \"token_type\": \"Bearer\",\n//            \"plan\": \"standard\",\n//            \"state\": \"...\"\n//    }\n\n\n    public String getAccess_token() {\n        return access_token;\n    }\n\n    public void setAccess_token(String access_token) {\n        this.access_token = access_token;\n    }\n\n    public String getRefresh_token() {\n        return refresh_token;\n    }\n\n    public void setRefresh_token(String refresh_token) {\n        this.refresh_token = refresh_token;\n    }\n\n    public String getToken_type() {\n//        if(!TextUtils.isEmpty(token_type)){\n//            token_type = token_type.substring(0, 1).toUpperCase() + token_type.substring(1);\n//        }\n        return token_type;\n    }\n\n    public void setToken_type(String token_type) {\n        KLog.e(\"授权吗A：\" + token_type);\n        if (!TextUtils.isEmpty(token_type)) {\n            token_type = token_type.substring(0, 1).toUpperCase() + token_type.substring(1);\n        }\n        this.token_type = token_type;\n    }\n\n    public long getExpires_in() {\n        return expires_in;\n    }\n\n    public void setExpires_in(long refresh_token) {\n        this.expires_in = expires_in;\n    }\n\n    public String getAuth() {\n        return token_type + \" \" + access_token;\n    }\n\n    public void setAuth(String auth) {\n        this.auth = auth;\n    }\n\n\n    @Override\n    public String toString() {\n        return \"Token{\" +\n                \"access_token='\" + access_token + '\\'' +\n                \", refresh_token='\" + refresh_token + '\\'' +\n                \", token_type='\" + token_type + '\\'' +\n                \", Auth='\" + getAuth() + '\\'' +\n                \", expires_in=\" + expires_in +\n                \", error='\" + error + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/UserAgent.java",
    "content": "package me.wizos.loread.bean;\n\n\n/**\n * @author Wizos on 2018/6/28.\n */\n\npublic class UserAgent {\n    private String name;\n    private String value;\n\n    public UserAgent(String name, String value) {\n        this.name = name;\n        this.value = value;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getValue() {\n        return value;\n    }\n\n    public void setValue(String value) {\n        this.value = value;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/domain/OutFeed.java",
    "content": "package me.wizos.loread.bean.domain;\n\n/**\n * Created by Wizos on 2019/5/15.\n */\n\npublic class OutFeed {\n    private String title;\n    private String feedUrl;\n    private String htmlUrl;\n\n    public OutFeed(String title, String feedUrl, String htmlUrl) {\n        this.title = title;\n        this.feedUrl = feedUrl;\n        this.htmlUrl = htmlUrl;\n    }\n\n    public String getTitle() {\n        return title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    public String getFeedUrl() {\n        return feedUrl;\n    }\n\n    public void setFeedUrl(String feedUrl) {\n        this.feedUrl = feedUrl;\n    }\n\n    public String getHtmlUrl() {\n        return htmlUrl;\n    }\n\n    public void setHtmlUrl(String htmlUrl) {\n        this.htmlUrl = htmlUrl;\n    }\n\n    @Override\n    public String toString() {\n        return \"OutFeed{\" +\n                \"title='\" + title + '\\'' +\n                \", feedUrl='\" + feedUrl + '\\'' +\n                \", htmlUrl='\" + htmlUrl + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/domain/OutTag.java",
    "content": "package me.wizos.loread.bean.domain;\n\nimport java.util.ArrayList;\n\n/**\n * Created by Wizos on 2019/5/15.\n */\n\npublic class OutTag {\n    private String title;\n    private ArrayList<OutFeed> outFeeds;\n\n    public String getTitle() {\n        return title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    public ArrayList<OutFeed> getOutFeeds() {\n        return outFeeds;\n    }\n\n    public void setOutFeeds(ArrayList<OutFeed> outFeeds) {\n        this.outFeeds = outFeeds;\n    }\n\n    public OutTag addOutFeed(OutFeed outFeed) {\n        if (outFeeds == null) {\n            outFeeds = new ArrayList<>();\n        }\n        outFeeds.add(outFeed);\n        return this;\n    }\n\n\n    @Override\n    public String toString() {\n        return \"OutTag{\" +\n                \"title='\" + title + '\\'' +\n                \", outFeeds=\" + outFeeds +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/CategoryItem.java",
    "content": "package me.wizos.loread.bean.feedly;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport me.wizos.loread.db.Category;\n\n/**\n * Created by Wizos on 2019/2/8.\n */\n\npublic class CategoryItem {\n    @SerializedName(\"id\")\n    private String id;\n\n    @SerializedName(\"label\")\n    private String label;\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getLabel() {\n        return label;\n    }\n\n    public void setLabel(String label) {\n        this.label = label;\n    }\n\n\n    public Category convert() {\n        Category category = new Category();\n        category.setId(id);\n        category.setTitle(label);\n        return category;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/Collection.java",
    "content": "package me.wizos.loread.bean.feedly;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.ArrayList;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.db.Category;\nimport me.wizos.loread.db.Feed;\n\n/**\n * Created by Wizos on 2019/2/8.\n */\n\npublic class Collection {\n    @SerializedName(\"id\")\n    private String id;\n\n    @SerializedName(\"label\")\n    private String label;\n\n    @SerializedName(\"customizable\")\n    private boolean customizable;\n\n    @SerializedName(\"enterprise\")\n    private boolean enterprise;\n\n    @SerializedName(\"numFeeds\")\n    private int numFeeds;\n\n    @SerializedName(\"feeds\")\n    private ArrayList<FeedItem> feedItems;\n\n    // 可选，大部分时候没有\n//    String description;\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getLabel() {\n        return label;\n    }\n\n    public void setLabel(String label) {\n        this.label = label;\n    }\n\n    public boolean isCustomizable() {\n        return customizable;\n    }\n\n    public void setCustomizable(boolean customizable) {\n        this.customizable = customizable;\n    }\n\n    public boolean isEnterprise() {\n        return enterprise;\n    }\n\n    public void setEnterprise(boolean enterprise) {\n        this.enterprise = enterprise;\n    }\n\n    public int getNumFeeds() {\n        return numFeeds;\n    }\n\n    public void setNumFeeds(int numFeeds) {\n        this.numFeeds = numFeeds;\n    }\n\n    public ArrayList<FeedItem> getFeedItems() {\n        return feedItems;\n    }\n\n    public void setFeedItems(ArrayList<FeedItem> feedItems) {\n        this.feedItems = feedItems;\n    }\n\n\n    public CategoryItem getCategoryItem() {\n        CategoryItem categoryItem = new CategoryItem();\n        categoryItem.setId(id);\n        categoryItem.setLabel(label);\n        return categoryItem;\n    }\n\n    public Category getCategory() {\n        Category category = new Category();\n        category.setId(id);\n        category.setTitle(label);\n        return category;\n    }\n\n    public ArrayList<Feed> getFeeds() {\n        ArrayList<Feed> feeds = new ArrayList<>(feedItems.size());\n        Feed feed;\n        for (FeedItem feedItem : feedItems) {\n            feed = feedItem.convert2Feed();\n            feed.setUid(App.i().getUser().getId());\n            feeds.add(feed);\n        }\n        return feeds;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/ContentDirection.java",
    "content": "package me.wizos.loread.bean.feedly;\n\nimport com.google.gson.annotations.SerializedName;\n\n/**\n * Created by Wizos on 2019/2/8.\n */\n\npublic class ContentDirection {\n    @SerializedName(\"content\")\n    String content;\n    @SerializedName(\"direction\")\n    String direction;\n\n    public String getContent() {\n        return content;\n    }\n\n    public void setContent(String content) {\n        this.content = content;\n    }\n\n    public String getDirection() {\n        return direction;\n    }\n\n    public void setDirection(String direction) {\n        this.direction = direction;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/Counts.java",
    "content": "package me.wizos.loread.bean.feedly;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.ArrayList;\n\n/**\n * Created by Wizos on 2019/2/8.\n */\n\npublic class Counts {\n    @SerializedName(\"unreadcounts\")\n    private ArrayList<Unreadcount> unreadcounts;\n    @SerializedName(\"updated\")\n    private long updated;\n\n    public ArrayList<Unreadcount> getUnreadcounts() {\n        return unreadcounts;\n    }\n\n    public void setUnreadcounts(ArrayList<Unreadcount> unreadcounts) {\n        this.unreadcounts = unreadcounts;\n    }\n\n    public long getUpdated() {\n        return updated;\n    }\n\n    public void setUpdated(long updated) {\n        this.updated = updated;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/Entry.java",
    "content": "package me.wizos.loread.bean.feedly;\n\nimport android.text.TextUtils;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.ArrayList;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.bean.Enclosure;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.network.api.BaseApi;\nimport me.wizos.loread.utils.ArticleUtil;\n\n/**\n * @author Wizos on 2019/2/8.\n */\n\npublic class Entry {\n    /**\n     * 文章的唯一的、不可变的ID。\n     */\n    private String id;\n    /**\n     * 在 RSS feed 中，这篇文章的唯一id (不一定是URL！也可能是文本数字)\n     */\n    private String originId;\n    /**\n     * 文章指纹。如果文章被更新，这个值可能会改变。\n     */\n    private String fingerprint;\n    /**\n     * 可选。文章的标题。此字符串不包含任何HTML标记。\n     */\n    private String title;\n\n    /**\n     * 可选。文章作者的名字\n     */\n    private String author;\n\n    /**\n     * 可选。文章内容的 object 。这个对象通常有两个值:“content”代表内容本身，“direction”(“ltr”代表从左到右，“rtl”代表从右到左)。内容本身包含经过净化的HTML标记。\n     */\n    private ContentDirection content;\n\n    /**\n     * 可选。文章概要的 object 。这个对象通常有两个值:“content”代表内容本身，“direction”(“ltr”代表从左到右，“rtl”代表从右到左)。内容本身包含经过净化的HTML标记。\n     */\n    private ContentDirection summary;\n\n    /**\n     * 这篇文章发表时的时间戳，单位为毫秒(通常不准确)。\n     */\n    private long published;\n    /**\n     * 可空。时间戳。这篇文章更新时的时间戳，单位为毫秒\n     */\n    private long updated;\n\n    /**\n     * 时间戳，单位为毫秒。当这篇文章被feedly Cloud服务器处理时，不可变的时间戳。\n     */\n    @SerializedName(\"crawled\")\n    private long crawled;\n\n//    /**\n//     * 可空。时间戳，以毫秒为单位。这篇文章被feedly Cloud服务器重新处理和更新时的时间戳。\n//     */\n//    private long recrawled;\n    /**\n     * 可空。origin对象是本文的爬取源。如果存在，“streamId”将包含feedId，“title”将包含feedTitle，“htmlUrl”将包含提要的网站。\n     */\n    private Origin origin;\n\n    // 可能为空\n    private String canonicalUrl;\n\n//    // 可能为空，不知用处\n//    @SerializedName(\"ampUrl\")\n//    private String ampUrl;\n//\n//    // 可能为空，不知用处\n//    @SerializedName(\"cdnAmpUrl\")\n//    private String cdnAmpUrl;\n\n    /**\n     * 可空。链接对象数组。本文的原始(?)链接列表。\n     */\n    private ArrayList<Enclosure> canonical;\n\n    /**\n     * 可空。链接对象数组。本文的替代链接列表。每个链接对象包含一种媒体类型和一个URL。通常，存在单个对象，其中包含到原始网页的链接。\n     */\n    private ArrayList<Enclosure> alternate;\n\n    /**\n     * 可空。由feed提供的的媒体链接列表(视频、图像、声音等)。有些条目没有摘要或内容，只有媒体链接的集合。\n     */\n    private ArrayList<Enclosure> enclosure;\n\n//    /**\n//     * 视觉对象。这个条目的图像URL。如果存在，“url”将包含图像URL，“宽度”和“高度”其维度，“内容类型”其MIME类型。\n//     */\n//    private Visual visual;\n\n    private ArrayList<String> keywords;\n\n    /**\n     * 这个条目被用户读取了吗？如果 header 中未提供 Authorization，这将始终返回false。如果提供了，它将反映用户是否读过该条目。\n     */\n    private boolean unread;\n\n//    /**\n//     * 貌似仅出现在 StreamContents 接口中\n//     */\n//    @SerializedName(\"categories\")\n//    private ArrayList<TTRSSCategoryItem> categories;\n//    @SerializedName(\"tags\")\n//    private ArrayList<TTRSSCategoryItem> tags;\n\n    /*\n     * 其他可能的字段\n     *\n            \"recrawled\": 1549551157946,\n            \"updateCount\": 1,\n\n            \"Visual\": {\n                \"url\": \"none\"\n            },\n            \"unread\": false,\n            \"categories\": [\n                {\n                    \"id\": \"user/12cc057f-9891-4ab3-99da-86f2dee7f2f5/category/1_博谈\",\n                    \"label\": \"1_博谈\"\n                }\n            ],\n            \"tags\": [\n                {\n                    \"id\": \"user/12cc057f-9891-4ab3-99da-86f2dee7f2f5/tag/global.read\",\n                    \"label\": \"\"\n                }\n            ],\n            // 表明这个条目有多受欢迎。这个数字越高，越多的读者阅读、保存或分享了这个条目。\n            \"engagement\": 15,\n            \"engagementRate\": 15\n\n            \"thumbnail\": [\n                {\n                    \"url\": \"https://2.bp.blogspot.com/-Jdp88f-UJ5c/XHFxq_pvD7I/AAAAAAAADHI/4z0axErIqq0FAgbIaTlveAodRWBf2EfkACK4BGAYYCw/s72-c/usa-dollar.jpg\",\n                    \"width\": 72,\n                    \"height\": 72\n                }\n            ],\n\n     手动添加的文章，通过“/entries/.mget”接口会包含以下字段\n\n        \"createdBy\": {\n            \"userAgent\": \"PostmanRuntime/7.4.0\",\n            \"application\": \"Feedly Developer\"\n        },\n        \"canonicalUrl\": \"https://www.theverge.com/2013/4/17/4236096/nbc-heroes-may-get-a-second-lease-on-life-on-xbox-live\",\n        \"ampUrl\": \"https://www.theverge.com/platform/amp/2013/4/17/4236096/nbc-heroes-may-get-a-second-lease-on-life-on-xbox-live\",\n        \"cdnAmpUrl\": \"https://www-theverge-com.cdn.ampproject.org/c/s/www.theverge.com/platform/amp/2013/4/17/4236096/nbc-heroes-may-get-a-second-lease-on-life-on-xbox-live\",\n\n     */\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getOriginId() {\n        return originId;\n    }\n\n    public void setOriginId(String originId) {\n        this.originId = originId;\n    }\n\n    public String getFingerprint() {\n        return fingerprint;\n    }\n\n    public void setFingerprint(String fingerprint) {\n        this.fingerprint = fingerprint;\n    }\n\n    public String getTitle() {\n        return title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    public String getAuthor() {\n        return author;\n    }\n\n    public void setAuthor(String author) {\n        this.author = author;\n    }\n\n    public ContentDirection getContent() {\n        return content;\n    }\n\n    public void setContent(ContentDirection content) {\n        this.content = content;\n    }\n\n    public ContentDirection getSummary() {\n        return summary;\n    }\n\n    public void setSummary(ContentDirection summary) {\n        this.summary = summary;\n    }\n\n    public long getPublished() {\n        return published;\n    }\n\n    public void setPublished(long published) {\n        this.published = published;\n    }\n\n    public long getCrawled() {\n        return crawled;\n    }\n\n    public void setCrawled(long crawled) {\n        this.crawled = crawled;\n    }\n\n    public long getUpdated() {\n        return updated;\n    }\n\n    public void setUpdated(long updated) {\n        this.updated = updated;\n    }\n\n    public Origin getOrigin() {\n        return origin;\n    }\n\n    public void setOrigin(Origin origin) {\n        this.origin = origin;\n    }\n\n    public String getCanonicalUrl() {\n        return canonicalUrl;\n    }\n\n    public void setCanonicalUrl(String canonicalUrl) {\n        this.canonicalUrl = canonicalUrl;\n    }\n\n    public ArrayList<Enclosure> getCanonical() {\n        return canonical;\n    }\n\n    public void setCanonical(ArrayList<Enclosure> canonical) {\n        this.canonical = canonical;\n    }\n\n    public ArrayList<Enclosure> getAlternate() {\n        return alternate;\n    }\n\n    public void setAlternate(ArrayList<Enclosure> alternate) {\n        this.alternate = alternate;\n    }\n\n    public ArrayList<Enclosure> getEnclosure() {\n        return enclosure;\n    }\n\n    public void setEnclosure(ArrayList<Enclosure> enclosure) {\n        this.enclosure = enclosure;\n    }\n\n    public ArrayList<String> getKeywords() {\n        return keywords;\n    }\n\n    public void setKeywords(ArrayList<String> keywords) {\n        this.keywords = keywords;\n    }\n\n    public boolean isUnread() {\n        return unread;\n    }\n\n    public void setUnread(boolean unread) {\n        this.unread = unread;\n    }\n\n    public Article convert(BaseApi.ArticleChanger articleChanger) {\n        Article article = new Article();\n        article.setId(id);\n\n        title = ArticleUtil.getOptimizedTitle(title);\n        article.setTitle(title);\n\n        article.setAuthor(author);\n        article.setPubDate(published);\n\n        if (alternate != null && alternate.size() > 0) {\n            article.setLink(alternate.get(0).getHref());\n        }\n        if (origin != null) {\n            article.setFeedId(origin.getStreamId());\n            article.setFeedTitle(origin.getTitle());\n        }\n\n        String tmpContent = \"\";\n        if (content != null && !TextUtils.isEmpty(content.getContent())) {\n            tmpContent = ArticleUtil.getOptimizedContent(article.getLink(), content.getContent());\n        } else if (summary != null && !TextUtils.isEmpty(summary.getContent())) {\n            tmpContent = ArticleUtil.getOptimizedContent(article.getLink(), summary.getContent());\n        }\n        tmpContent = ArticleUtil.getOptimizedContentWithEnclosures(tmpContent,enclosure);\n        article.setContent(tmpContent);\n\n        String tmpSummary = ArticleUtil.getOptimizedSummary(tmpContent);\n        article.setSummary(tmpSummary);\n\n        String coverUrl = ArticleUtil.getCoverUrl(article.getLink(),tmpContent);\n        article.setImage(coverUrl);\n\n        // 自己设置的字段\n        // KLog.i(\"【增加文章】\" + article.getId());\n        article.setSaveStatus(App.STATUS_NOT_FILED);\n        if (articleChanger != null) {\n            articleChanger.change(article);\n        }\n        return article;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/FeedItem.java",
    "content": "package me.wizos.loread.bean.feedly;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.ArrayList;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.db.Feed;\n\n/**\n * 以搜索知乎，feed/http://zhihurss.miantiao.me/section/id/2 源为例\n * Created by Wizos on 2019/2/8.\n */\n\npublic class FeedItem {\n    private String id;\n    private String feedId;\n    private String title;\n    private String description;\n    private String website;\n    private String iconUrl; // 可能为空，小图\n    private String visualUrl; // 可能为空，大图\n    private String language; // 值可能为：zh，en\n    private int subscribers;\n    private long updated;\n    private float velocity; // 每周发布的文章的平均数量。 此号码每隔几天更新一次\n    private boolean partial; // 可能为空；部分的; 偏爱的\n    private String contentType; // 可能为空；可能为 article， longform\n    private String state; // 可能为空。值可能为：dead.stale，dormant\n    private ArrayList<String> topics; // 可能为空\n\n    // 单独获取feed信息时可见(批量接口)\n    // private int estimatedEngagement;\n\n    // 搜索时可见\n    // private long lastUpdated; // 可能用不到吧，和 Updated 字段类似\n    // private float score;\n    // private int coverage;\n    // private int coverageScore;\n    // private int averageReadTime;\n    // private String websiteTitle;\n    // private int totalTagCount;\n    // private ArrayList<> tagCounts;\n    // private ArrayList<String> deliciousTags;\n\n    // 以下不常见到\n    // String coverColor;\n    // String logo;\n    // String relatedLayout;\n    // String relatedTarget;\n\n    public String getId() {\n        return id;\n    }\n    public void setId(String id) {\n        this.id = id;\n    }\n    public String getFeedId() {\n        return feedId;\n    }\n    public void setFeedId(String feedId) {\n        this.feedId = feedId;\n    }\n    public String getTitle() {\n        return title;\n    }\n    public void setTitle(String title) {\n        this.title = title;\n    }\n    public String getDescription() {\n        return description;\n    }\n    public void setDescription(String description) {\n        this.description = description;\n    }\n    public String getWebsite() {\n        return website;\n    }\n    public void setWebsite(String website) {\n        this.website = website;\n    }\n    public String getIconUrl() {\n        return iconUrl;\n    }\n    public void setIconUrl(String iconUrl) {\n        this.iconUrl = iconUrl;\n    }\n    public String getVisualUrl() {\n        return visualUrl;\n    }\n    public void setVisualUrl(String visualUrl) {\n        this.visualUrl = visualUrl;\n    }\n    public String getLanguage() {\n        return language;\n    }\n    public void setLanguage(String language) {\n        this.language = language;\n    }\n    public int getSubscribers() {\n        return subscribers;\n    }\n    public void setSubscribers(int subscribers) {\n        this.subscribers = subscribers;\n    }\n    public long getUpdated() {\n        return updated;\n    }\n    public void setUpdated(long updated) {\n        this.updated = updated;\n    }\n    public float getVelocity() {\n        return velocity;\n    }\n    public void setVelocity(float velocity) {\n        this.velocity = velocity;\n    }\n    public ArrayList<String> getTopics() {\n        return topics;\n    }\n    public void setTopics(ArrayList<String> topics) {\n        this.topics = topics;\n    }\n    public boolean isPartial() {\n        return partial;\n    }\n    public void setPartial(boolean partial) {\n        this.partial = partial;\n    }\n    public String getContentType() {\n        return contentType;\n    }\n    public void setContentType(String contentType) {\n        this.contentType = contentType;\n    }\n    public String getState() {\n        return state;\n    }\n    public void setState(String state) {\n        this.state = state;\n    }\n\n    public Feed convert2Feed() {\n        Feed feed = new Feed();\n        feed.setId(id);\n        feed.setTitle(title);\n        feed.setFeedUrl(id.substring(5));\n        feed.setHtmlUrl(website);\n        feed.setIconUrl(visualUrl);\n        feed.setDisplayMode(App.OPEN_MODE_RSS);\n        return feed;\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return \"TTRSSFeedItem{\" +\n                \"id='\" + id + '\\'' +\n                \", feedId='\" + feedId + '\\'' +\n                \", title='\" + title + '\\'' +\n                \", description='\" + description + '\\'' +\n                \", website='\" + website + '\\'' +\n                \", iconUrl='\" + iconUrl + '\\'' +\n                \", visualUrl='\" + visualUrl + '\\'' +\n                \", language='\" + language + '\\'' +\n                \", subscribers=\" + subscribers +\n                \", updated=\" + updated +\n                \", velocity=\" + velocity +\n                \", partial=\" + partial +\n                \", contentType='\" + contentType + '\\'' +\n                \", state='\" + state + '\\'' +\n                \", topics=\" + topics +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/Origin.java",
    "content": "package me.wizos.loread.bean.feedly;\n\nimport com.google.gson.annotations.SerializedName;\n\n/**\n * Created by Wizos on 2019/2/8.\n */\n\npublic class Origin {\n    @SerializedName(\"streamId\")\n    String streamId;\n    @SerializedName(\"title\")\n    String title;\n    @SerializedName(\"htmlUrl\")\n    String htmlUrl;\n\n    public String getStreamId() {\n        return streamId;\n    }\n\n    public void setStreamId(String streamId) {\n        this.streamId = streamId;\n    }\n\n    public String getHtmlUrl() {\n        return htmlUrl;\n    }\n\n    public void setHtmlUrl(String htmlUrl) {\n        this.htmlUrl = htmlUrl;\n    }\n\n    public String getTitle() {\n        return title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/Profile.java",
    "content": "package me.wizos.loread.bean.feedly;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport me.wizos.loread.Contract;\nimport me.wizos.loread.db.User;\n\n/**\n * @author Wizos on 2019/2/11.\n */\n\npublic class Profile {\n\n    /*\n    \"id\": \"12cc057f-9891-4ab3-99da-86f2dee7f2f5\",\n    \"client\": \"feedly\",\n    \"created\": 1457934974286,\n    \"email\": \"wizos@qq.com\",\n    \"wave\": \"2016.12\",\n    \"verified\": true,\n    \"login\": \"wizos@qq.com\",\n    \"logins\": [\n        {\n            \"id\": \"wizos@qq.com\",\n            \"verified\": false,\n            \"provider\": \"FeedlyLogin\",\n            \"providerId\": \"wizos@qq.com\"\n        }\n    ],\n    \"refPage\": \"welcome\",\n    \"landingPage\": \"welcome\",\n    \"dropboxConnected\": false,\n    \"twitterConnected\": false,\n    \"facebookConnected\": false,\n    \"evernoteConnected\": false,\n    \"pocketConnected\": false,\n    \"wordPressConnected\": false,\n    \"windowsLiveConnected\": false,\n    \"instapaperConnected\": false,\n    \"source\": \"feedly.desktop 30.0.1117\",\n    \"fullName\": \"wizos\"\n     */\n\n    @SerializedName(\"id\")\n    private String id;\n    @SerializedName(\"login\")\n    private String login;\n    @SerializedName(\"email\")\n    private String email;\n    @SerializedName(\"fullName\")\n    private String fullName;\n\n    public User getUser() {\n        User user = new User();\n        user.setSource(Contract.PROVIDER_FEEDLY);\n        user.setId(Contract.PROVIDER_FEEDLY + \"_\" + id);\n        user.setUserId(id);\n        user.setUserEmail(email);\n        user.setUserName(fullName);\n        return user;\n    }\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getLogin() {\n        return login;\n    }\n\n    public void setLogin(String login) {\n        this.login = login;\n    }\n\n    public String getEmail() {\n        return email;\n    }\n\n    public void setEmail(String email) {\n        this.email = email;\n    }\n\n    public String getFullName() {\n        return fullName;\n    }\n\n    public void setFullName(String fullName) {\n        this.fullName = fullName;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/StreamContents.java",
    "content": "package me.wizos.loread.bean.feedly;\n\nimport java.util.ArrayList;\n\n/**\n * Created by Wizos on 2019/2/8.\n */\n\npublic class StreamContents {\n    private String id;\n    private String title;\n    private long updated;\n    private String continuation;\n    private ArrayList<Entry> items;\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public long getUpdated() {\n        return updated;\n    }\n\n    public void setUpdated(long updated) {\n        this.updated = updated;\n    }\n\n    public String getContinuation() {\n        return continuation;\n    }\n\n    public void setContinuation(String continuation) {\n        this.continuation = continuation;\n    }\n\n    public ArrayList<Entry> getItems() {\n        return items;\n    }\n\n    public void setItems(ArrayList<Entry> items) {\n        this.items = items;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/StreamIds.java",
    "content": "package me.wizos.loread.bean.feedly;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.ArrayList;\n\n/**\n * Created by Wizos on 2019/2/8.\n */\n\npublic class StreamIds {\n    @SerializedName(\"ids\")\n    private ArrayList<String> ids;\n\n    @SerializedName(\"continuation\")\n    private String continuation;\n\n    public ArrayList<String> getIds() {\n        return ids;\n    }\n\n    public void setIds(ArrayList<String> ids) {\n        this.ids = ids;\n    }\n\n    public String getContinuation() {\n        return continuation;\n    }\n\n    public void setContinuation(String continuation) {\n        this.continuation = continuation;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/Subscription.java",
    "content": "//package me.wizos.loreadx.bean.feedly;\n//\n//import com.google.gson.annotations.SerializedName;\n//\n//import java.util.ArrayList;\n//\n///**\n// * Created by Wizos on 2019/2/8.\n// */\n//\n//public class Subscription {\n//    private String id;\n//    private String title;\n//    private String website;\n//    // 可能为空\n//    private String iconUrl;\n//    // 可能为空\n//    private String visualUrl;\n//    private String description;\n//\n//    // 值可能为：zh，en\n//    private String language;\n//    private int subscribers;\n//    private long updated;\n//    private float velocity;\n//    // 可能为空；部分的; 偏爱的\n//    private boolean partial;\n//    // 可能为空；可能为 article， longform\n//    private String contentType;\n//    // 可能为空。值可能为：dead.stale，dormant\n//    private String state;\n//    // 可能为空\n//    private ArrayList<String> topics;\n//\n//    private ArrayList<TTRSSCategoryItem> categories;\n//\n//    // estimatedEngagement\n//    // coverUrl\n//    // logo\n//    // coverColor\n//    // relatedLayout\n//    // relatedTarget\n//\n//\n//\n//\n//    public String getId() {\n//        return id;\n//    }\n//\n//    public void setId(String id) {\n//        this.id = id;\n//    }\n//\n//    public String getTitle() {\n//        return title;\n//    }\n//\n//    public void setTitle(String title) {\n//        this.title = title;\n//    }\n//\n//    public String getWebsite() {\n//        return website;\n//    }\n//\n//    public void setWebsite(String website) {\n//        this.website = website;\n//    }\n//\n//    public String getIconUrl() {\n//        return iconUrl;\n//    }\n//\n//    public void setIconUrl(String iconUrl) {\n//        this.iconUrl = iconUrl;\n//    }\n//\n//    public String getVisualUrl() {\n//        return visualUrl;\n//    }\n//\n//    public void setVisualUrl(String visualUrl) {\n//        this.visualUrl = visualUrl;\n//    }\n//\n//    public int getSubscribers() {\n//        return subscribers;\n//    }\n//\n//    public void setSubscribers(int subscribers) {\n//        this.subscribers = subscribers;\n//    }\n//\n//    public long getUpdated() {\n//        return updated;\n//    }\n//\n//    public void setUpdated(long updated) {\n//        this.updated = updated;\n//    }\n//\n//    public float getVelocity() {\n//        return velocity;\n//    }\n//\n//    public void setVelocity(float velocity) {\n//        this.velocity = velocity;\n//    }\n//\n//    public boolean isPartial() {\n//        return partial;\n//    }\n//\n//    public void setPartial(boolean partial) {\n//        this.partial = partial;\n//    }\n//\n//    public String getContentType() {\n//        return contentType;\n//    }\n//\n//    public void setContentType(String contentType) {\n//        this.contentType = contentType;\n//    }\n//\n//    public String getState() {\n//        return state;\n//    }\n//\n//    public void setState(String state) {\n//        this.state = state;\n//    }\n//\n//    public ArrayList<String> getTopics() {\n//        return topics;\n//    }\n//\n//    public void setTopics(ArrayList<String> topics) {\n//        this.topics = topics;\n//    }\n//\n//    public ArrayList<TTRSSCategoryItem> getCategoryItems() {\n//        return categories;\n//    }\n//\n//    public void setCategoryItems(ArrayList<TTRSSCategoryItem> categories) {\n//        this.categories = categories;\n//    }\n//\n//    public String getDescription() {\n//        return description;\n//    }\n//\n//    public void setDescription(String description) {\n//        this.description = description;\n//    }\n//\n//    public String getLanguage() {\n//        return language;\n//    }\n//\n//    public void setLanguage(String language) {\n//        this.language = language;\n//    }\n//\n//    @Override\n//    public String toString() {\n//        return \"Subscription{\" +\n//                \"id='\" + id + '\\'' +\n//                \", title='\" + title + '\\'' +\n//                \", website='\" + website + '\\'' +\n//                \", iconUrl='\" + iconUrl + '\\'' +\n//                \", visualUrl='\" + visualUrl + '\\'' +\n//                \", description='\" + description + '\\'' +\n//                \", language='\" + language + '\\'' +\n//                \", subscribers=\" + subscribers +\n//                \", updated=\" + updated +\n//                \", velocity=\" + velocity +\n//                \", partial=\" + partial +\n//                \", contentType='\" + contentType + '\\'' +\n//                \", state='\" + state + '\\'' +\n//                \", topics=\" + topics +\n//                \", categories=\" + categories +\n//                '}';\n//    }\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/Unreadcount.java",
    "content": "package me.wizos.loread.bean.feedly;\n\nimport com.google.gson.annotations.SerializedName;\n\n/**\n * Created by Wizos on 2019/2/8.\n */\n\npublic class Unreadcount {\n    @SerializedName(\"id\")\n    private String id;\n    @SerializedName(\"count\")\n    private int count;\n    @SerializedName(\"updated\")\n    private long updated;\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public int getCount() {\n        return count;\n    }\n\n    public void setCount(int count) {\n        this.count = count;\n    }\n\n    public long getUpdated() {\n        return updated;\n    }\n\n    public void setUpdated(long updated) {\n        this.updated = updated;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/Visual.java",
    "content": "package me.wizos.loread.bean.feedly;\n\nimport com.google.gson.annotations.SerializedName;\n\n/**\n * Created by Wizos on 2019/2/24.\n */\n\npublic class Visual {\n    /**\n     * 可能为none\n     */\n    @SerializedName(\"url\")\n    private Origin url;\n\n    /**\n     * 可空(可能性中)\n     */\n    @SerializedName(\"contentType\")\n    private Origin contentType;\n\n    /**\n     * 可空(可能性高)\n     */\n    @SerializedName(\"width\")\n    private int width;\n\n    /**\n     * 可空(可能性高)\n     */\n    @SerializedName(\"height\")\n    private int height;\n\n    public Origin getUrl() {\n        return url;\n    }\n\n    public void setUrl(Origin url) {\n        this.url = url;\n    }\n\n    public Origin getContentType() {\n        return contentType;\n    }\n\n    public void setContentType(Origin contentType) {\n        this.contentType = contentType;\n    }\n\n    public int getWidth() {\n        return width;\n    }\n\n    public void setWidth(int width) {\n        this.width = width;\n    }\n\n    public int getHeight() {\n        return height;\n    }\n\n    public void setHeight(int height) {\n        this.height = height;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/input/EditCollection.java",
    "content": "package me.wizos.loread.bean.feedly.input;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.ArrayList;\n\n/**\n * Created by Wizos on 2019/2/24.\n */\n\npublic class EditCollection {\n    /**\n     * 此集合的唯一标签；新类别所需，编辑现有类别时可选\n     */\n    @SerializedName(\"label\")\n    private String label;\n    /**\n     * 可空。Collection的标识。如果缺失，服务器将生成一个(新集合Collection)。\n     */\n    @SerializedName(\"id\")\n    private String id;\n    /**\n     * 这个集合的更详细的描述。\n     */\n    @SerializedName(\"description\")\n    private String description;\n    /**\n     * 可空。feed列表，表示要添加到此集合的feed列表。\n     */\n    // 必须没有categories\n    @SerializedName(\"feeds\")\n    private ArrayList<EditFeed> feeds;\n\n    public EditCollection(String id) {\n        this.id = id;\n    }\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getLabel() {\n        return label;\n    }\n\n    public void setLabel(String label) {\n        this.label = label;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    public void setDescription(String description) {\n        this.description = description;\n    }\n\n    public ArrayList<EditFeed> getFeeds() {\n        return feeds;\n    }\n\n    public void setFeeds(ArrayList<EditFeed> feeds) {\n        this.feeds = feeds;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/input/EditFeed.java",
    "content": "package me.wizos.loread.bean.feedly.input;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.bean.feedly.CategoryItem;\nimport me.wizos.loread.db.Category;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.Feed;\n\n/**\n * Created by Wizos on 2019/2/24.\n */\n\npublic class EditFeed {\n    private String id;\n    private String title;\n    private ArrayList<CategoryItem> categoryItems = new ArrayList<>();\n\n    public EditFeed() {\n    }\n\n\n    public EditFeed(String feedId) {\n        Feed feed = CoreDB.i().feedDao().getById(App.i().getUser().getId(), feedId);\n        id = feed.getId();\n        title = feed.getTitle();\n        List<Category> categories = CoreDB.i().categoryDao().getByFeedId(App.i().getUser().getId(), feedId);\n        for (Category category : categories) {\n            categoryItems.add(category.convert2CategoryItem());\n        }\n    }\n\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getTitle() {\n        return title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    public ArrayList<CategoryItem> getCategoryItems() {\n        return categoryItems;\n    }\n\n    public void setCategoryItems(ArrayList<CategoryItem> categoryItems) {\n        this.categoryItems = categoryItems;\n    }\n\n\n    @Override\n    public String toString() {\n        return \"EditFeed{\" +\n                \"id='\" + id + '\\'' +\n                \", title='\" + title + '\\'' +\n                \", categoryItems=\" + categoryItems +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/feedly/input/MarkerAction.java",
    "content": "package me.wizos.loread.bean.feedly.input;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.List;\n\n/**\n * Created by Wizos on 2019/2/24.\n */\n\npublic class MarkerAction {\n    public final static String MARK_AS_READ = \"markAsRead\";\n    public final static String MARK_AS_UNREAD = \"keepUnread\";\n    public final static String UNDO_MARK_AS_READ = \"undoMarkAsRead\";\n    public final static String MARK_AS_SAVED = \"markAsSaved\";\n    public final static String MARK_AS_UNSAVED = \"markAsUnsaved\";\n\n    public final static String TYPE_ENTRIES = \"entries\";\n    public final static String TYPE_FEEDS = \"feeds\";\n    public final static String TYPE_CATEGORIES = \"categories\";\n    public final static String TYPE_TAGS = \"tags\";\n\n    /**\n     * markAsRead，keepUnread，undoMarkAsRead，markAsSaved，markAsUnsaved\n     */\n    @SerializedName(\"action\")\n    private String action;\n    /**\n     * entries，feeds，categories，tags\n     */\n    @SerializedName(\"type\")\n    private String type;\n\n    @SerializedName(\"entryIds\")\n    private List<String> entryIds;\n\n    @SerializedName(\"feedIds\")\n    private List<String> feedIds;\n\n    @SerializedName(\"categoryIds\")\n    private List<String> categoryIds;\n\n//    @SerializedName(\"lastReadEntryId\")\n//    private String lastReadEntryId;\n//\n//    // 时间戳替代(不太准确)\n//    @SerializedName(\"asOf\")\n//    private long asOf;\n\n    public String getAction() {\n        return action;\n    }\n\n    public void setAction(String action) {\n        this.action = action;\n    }\n\n    public String getType() {\n        return type;\n    }\n\n    public void setType(String type) {\n        this.type = type;\n    }\n\n    public List<String> getEntryIds() {\n        return entryIds;\n    }\n\n    public void setEntryIds(List<String> entryIds) {\n        this.entryIds = entryIds;\n    }\n\n    public List<String> getFeedIds() {\n        return feedIds;\n    }\n\n    public void setFeedIds(List<String> feedIds) {\n        this.feedIds = feedIds;\n    }\n\n    public List<String> getCategoryIds() {\n        return categoryIds;\n    }\n\n    public void setCategoryIds(List<String> categoryIds) {\n        this.categoryIds = categoryIds;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/fever/BaseResponse.java",
    "content": "package me.wizos.loread.bean.fever;\n\nimport com.google.gson.annotations.SerializedName;\n\npublic class BaseResponse {\n    @SerializedName(\"api_version\")\n    private int apiVersion;\n    @SerializedName(\"auth\")\n    private int auth; // 为 1 时，代表验证/授权成功\n    @SerializedName(\"last_refreshed_on_time\")\n    private long lastRefreshedOnTime;\n    @SerializedName(\"error\")\n    private String error; // NOT_LOGGED_IN\n\n    public int getApiVersion() {\n        return apiVersion;\n    }\n\n    public int getAuth() {\n        return auth;\n    }\n\n    public long getLastRefreshedOnTime() {\n        return lastRefreshedOnTime;\n    }\n\n\n    public boolean isSuccessful(){\n        return auth == 1;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/fever/Feed.java",
    "content": "//package me.wizos.loreadx.bean.fever;\n//\n//import com.google.gson.annotations.SerializedName;\n//\n//import me.wizos.loreadx.App;\n//\n//public class Feed {\n//    @SerializedName(\"id\")\n//    private int id;\n//    @SerializedName(\"favicon_id\")\n//    private int faviconId;\n//    @SerializedName(\"title\")\n//    private String title;\n//    @SerializedName(\"url\")\n//    private String url;\n//    @SerializedName(\"site_url\")\n//    private String siteUrl;\n//    @SerializedName(\"is_spark\")\n//    private int isSpark;\n//    @SerializedName(\"last_updated_on_time\")\n//    private long lastUpdatedOnTime; // 到秒\n//\n//    public int getId() {\n//        return id;\n//    }\n//\n//    public int getFaviconId() {\n//        return faviconId;\n//    }\n//\n//    public String getTitle() {\n//        return title;\n//    }\n//\n//    public String getUrl() {\n//        return url;\n//    }\n//\n//    public String getSiteUrl() {\n//        return siteUrl;\n//    }\n//\n//    public int getIsSpark() {\n//        return isSpark;\n//    }\n//\n//    public long getLastUpdatedOnTime() {\n//        return lastUpdatedOnTime;\n//    }\n//\n//\n//    public Feed convert(){\n//        Feed feed = new Feed();\n//        feed.setId(\"feed/\" + id);\n//        feed.setTitle(title);\n//        feed.setFeedUrl(url);\n//        feed.setHtmlUrl(siteUrl);\n//        //feed.setIconUrl(visualUrl);\n//        feed.setOpenMode(App.OPEN_MODE_RSS);\n//        return feed;\n//    }\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/fever/Feeds.java",
    "content": "//package me.wizos.loreadx.bean.fever;\n//\n//import com.google.gson.annotations.SerializedName;\n//\n//import java.util.List;\n//\n//public class Feeds extends BaseResponse {\n//    @SerializedName(\"feeds\")\n//    private List<Feed> feeds;\n//    @SerializedName(\"feeds_groups\")\n//    private List<GroupFeeds> feedsGroups;\n//\n//    public List<Feed> getFeeds() {\n//        return feeds;\n//    }\n//\n//    public List<GroupFeeds> getFeedsGroups() {\n//        return feedsGroups;\n//    }\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/fever/Group.java",
    "content": "package me.wizos.loread.bean.fever;\n\nimport android.text.TextUtils;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport me.wizos.loread.db.Category;\n\npublic class Group {\n    private int id;\n    private String title;\n    @SerializedName(\"feed_ids\")\n    private String feedIds;\n\n    public int getId() {\n        return id;\n    }\n\n    public String getTitle() {\n        return title;\n    }\n\n    public String[] getFeedIds(){\n        if(TextUtils.isEmpty(feedIds)){\n            return null;\n        }else {\n            return feedIds.split(\",\");\n        }\n    }\n\n    public Category getCategry(){\n        Category category = new Category();\n        category.setId(\"user/\" + id);\n        category.setTitle(title);\n        return category;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/fever/GroupFeeds.java",
    "content": "package me.wizos.loread.bean.fever;\n\nimport com.google.gson.annotations.SerializedName;\n\npublic class GroupFeeds {\n    @SerializedName(\"group_id\")\n    private int groupId;\n    @SerializedName(\"feed_ids\")\n    private String feedIds;\n\n    public int getGroupId() {\n        return groupId;\n    }\n\n    public String getFeedIds() {\n        return feedIds;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/fever/Groups.java",
    "content": "package me.wizos.loread.bean.fever;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.List;\n\n/**\n * 超级组 Kindling 不在该响应中，它由所有 is_spark = 0 的 feed 组成\n * 超级组 Sparks 不在该响应中，它由所有 is_spark = 1 的 feed 组成\n */\n\npublic class Groups extends BaseResponse {\n    @SerializedName(\"groups\")\n    private List<Group> groups;\n\n    @SerializedName(\"feeds_groups\")\n    private List<GroupFeeds> feedsGroups;\n\n    public List<Group> getGroups() {\n        return groups;\n    }\n\n    public List<GroupFeeds> getFeedsGroups() {\n        return feedsGroups;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/fever/Item.java",
    "content": "package me.wizos.loread.bean.fever;\n\nimport com.google.gson.annotations.SerializedName;\n\npublic class Item {\n    @SerializedName(\"id\")\n    private int id;\n    @SerializedName(\"feed_id\")\n    private int feedId;\n    @SerializedName(\"title\")\n    private String title;\n    @SerializedName(\"author\")\n    private String author;\n    @SerializedName(\"html\")\n    private String html;\n\n    @SerializedName(\"url\")\n    private String url;\n\n    @SerializedName(\"is_saved\")\n    private int isSaved;\n    @SerializedName(\"is_read\")\n    private int isRead;\n\n    @SerializedName(\"created_on_time\")\n    private long createdOnTime;\n\n    public int getId() {\n        return id;\n    }\n\n    public int getFeedId() {\n        return feedId;\n    }\n\n    public String getTitle() {\n        return title;\n    }\n\n    public String getAuthor() {\n        return author;\n    }\n\n    public String getHtml() {\n        return html;\n    }\n\n    public String getUrl() {\n        return url;\n    }\n\n    public int getIsSaved() {\n        return isSaved;\n    }\n\n    public int getIsRead() {\n        return isRead;\n    }\n\n    public long getCreatedOnTime() {\n        return createdOnTime;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/fever/Items.java",
    "content": "package me.wizos.loread.bean.fever;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.List;\n\npublic class Items extends BaseResponse {\n    @SerializedName(\"total_items\")\n    private String totalItems;\n\n    @SerializedName(\"items\")\n    private List<Item> items;\n\n    public String getTotalItems() {\n        return totalItems;\n    }\n\n    public List<Item> getItems() {\n        return items;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/fever/SavedItemIds.java",
    "content": "package me.wizos.loread.bean.fever;\n\nimport android.text.TextUtils;\n\nimport com.google.gson.annotations.SerializedName;\n\npublic class SavedItemIds extends BaseResponse {\n    @SerializedName(\"saved_item_ids\")\n    private String savedItemIds;\n\n    public String[] getSavedItemIds(){\n        if(TextUtils.isEmpty(savedItemIds)){\n            return null;\n        }else {\n            return savedItemIds.split(\",\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/fever/UnreadItemIds.java",
    "content": "package me.wizos.loread.bean.fever;\n\nimport android.text.TextUtils;\n\nimport com.google.gson.annotations.SerializedName;\n\npublic class UnreadItemIds extends BaseResponse {\n    @SerializedName(\"unread_item_ids\")\n    private String unreadItemIds;\n    public String[] getUreadItemIds(){\n        if(TextUtils.isEmpty(unreadItemIds)){\n            return null;\n        }else {\n            return unreadItemIds.split(\",\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/EditTag.java",
    "content": "//package me.wizos.loreadx.bean.inoreader;\n//\n///**\n// * Created by Wizos on 2019/5/12.\n// */\n//\n//public class EditTag {\n//\n//    private String ac;\n//\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/GsItemContents.java",
    "content": "//package me.wizos.loread.bean.gson;\n//\n//import com.google.gson.annotations.SerializedName;\n//\n//import java.util.ArrayList;\n//\n//import me.wizos.loread.bean.gson.itemContents.Items;\n//import me.wizos.loread.bean.gson.itemContents.Self;\n//\n///**\n// * 该 api 貌似被官方弃用，改用 stream content 了\n// * Created by Wizos on 2016/3/11.\n// */\n//public class GsItemContents {\n//\n//    @SerializedName(\"direction\")\n//    String direction;\n//\n//    @SerializedName(\"id\")\n//    String id;\n//\n//    @SerializedName(\"title\")\n//    String title;\n//\n//    @SerializedName(\"description\")\n//    String description;\n//\n//    @SerializedName(\"self\")\n//    Self self;\n//\n//    @SerializedName(\"updated\")\n//    long updated;\n//\n//    @SerializedName(\"updatedUsec\")\n//    long updatedUsec;\n//\n//    @SerializedName(\"items\")\n//    ArrayList<Items> items;\n//\n//    @SerializedName(\"continuation\")\n//    String continuation;\n//\n//\n//    public String getDirection() {\n//        return direction;\n//    }\n//    public void setDirection(String direction) {\n//        this.direction = direction;\n//    }\n//\n//    public String getId() {\n//        return id;\n//    }\n//\n//    public void setId(String id) {\n//        this.id = id;\n//    }\n//\n//    public String getDescription() {\n//        return description;\n//    }\n//    public void setDescription(String description) {\n//        this.description = description;\n//    }\n//\n//    public String getTitle() {\n//        return title;\n//    }\n//    public void setTitle(String title) {\n//        this.title = title;\n//    }\n//\n//    public Self getSelf() {\n//        return self;\n//    }\n//    public void setSelf(Self self) {\n//        this.self = self;\n//    }\n//\n//    public long getUpdated() {\n//        return updated;\n//    }\n//    public void setUpdated(long updated) {\n//        this.updated = updated;\n//    }\n//\n//    public long getUpdatedUsec() {\n//        return updatedUsec;\n//    }\n//    public void setUpdatedUsec(long updatedUsec) {\n//        this.updatedUsec = updatedUsec;\n//    }\n//\n//    public ArrayList<Items> getItems() {\n//        return items;\n//    }\n//    public void setItems(ArrayList<Items> items) {\n//        this.items = items;\n//    }\n//\n//    public String getContinuation() {\n//        return continuation;\n//    }\n//    public void setContinuation(String continuation) {\n//        this.continuation = continuation;\n//    }\n//}\n//\n//\n//\n//\n//\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/GsTag.java",
    "content": "package me.wizos.loread.bean.inoreader;\n\nimport com.google.gson.annotations.SerializedName;\n\n/**\n * Created by Wizos on 2019/2/20.\n */\n\npublic class GsTag {\n    @SerializedName(\"id\")\n    String id;\n    @SerializedName(\"sortid\")\n    String sortid;\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getSortid() {\n        return sortid;\n    }\n\n    public void setSortid(String sortid) {\n        this.sortid = sortid;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/GsTags.java",
    "content": "package me.wizos.loread.bean.inoreader;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport org.parceler.Parcel;\n\nimport java.util.ArrayList;\n\nimport me.wizos.loread.db.Category;\n\n@Parcel\npublic class GsTags {\n    @SerializedName(\"tags\")\n    ArrayList<Category> categories;\n\n    public ArrayList<Category> getCategories() {\n        return categories;\n    }\n\n    public void setCategories(ArrayList<Category> categories) {\n        this.categories = categories;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/GsUnreadCount.java",
    "content": "package me.wizos.loread.bean.inoreader;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.ArrayList;\n\n/**\n * Created by Wizos on 2016/3/11.\n */\npublic class GsUnreadCount {\n    @SerializedName(\"max\")\n    int max;\n\n    @SerializedName(\"unreadcounts\")\n    ArrayList<UnreadCounts> unreadcounts;\n\n    public int getMax() {\n        return max;\n    }\n\n    public void setMax(int max) {\n        this.max = max;\n    }\n\n    public ArrayList<UnreadCounts> getUnreadcounts() {\n        return unreadcounts;\n    }\n\n    public void setUnreadcounts(ArrayList<UnreadCounts> unreadcounts) {\n        this.unreadcounts = unreadcounts;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/ItemIds.java",
    "content": "package me.wizos.loread.bean.inoreader;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.ArrayList;\n\n/**\n * Created by Wizos on 2016/3/10.\n */\npublic class ItemIds {\n    @SerializedName(\"items\")\n    ArrayList<String> items;\n\n    @SerializedName(\"itemRefs\")\n    ArrayList<ItemRefs> itemRefs;\n\n    @SerializedName(\"continuation\")\n    String continuation;\n\n    public ItemIds() {\n        items = new ArrayList<>();\n        itemRefs = new ArrayList<>();\n        continuation = null;\n    }\n\n\n    public ArrayList<String> getItems() {\n        return items;\n    }\n\n    public void setItems(ArrayList<String> items) {\n        this.items = items;\n    }\n\n    public void setItemRefs(ArrayList<ItemRefs> itemRefs) {\n        this.itemRefs = itemRefs;\n    }\n\n    public ArrayList<ItemRefs> getItemRefs() {\n        return itemRefs;\n    }\n\n    public void setContinuation(String continuation) {\n        this.continuation = continuation;\n    }\n\n    public String getContinuation() {\n        return continuation;\n    }\n\n    public void addItemRefs(ArrayList<ItemRefs> itemRefs) {\n        this.itemRefs.addAll(itemRefs);\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/ItemRefs.java",
    "content": "package me.wizos.loread.bean.inoreader;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.ArrayList;\n\n/**\n * Created by Wizos on 2016/3/10.\n */\npublic class ItemRefs {\n\n    @SerializedName(\"id\")\n    String id;\n\n    @SerializedName(\"directStreamIds\")\n    ArrayList<String> directStreamIds;\n\n    @SerializedName(\"timestampUsec\")\n    long timestampUsec;\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public void setDirectStreamIds(ArrayList<String> directStreamIds) {\n        this.directStreamIds = directStreamIds;\n    }\n\n    public ArrayList<String> getDirectStreamIds() {\n        return directStreamIds;\n    }\n\n    public void setTimestampUsec(long timestampUsec) {\n        this.timestampUsec = timestampUsec;\n    }\n\n    public long getTimestampUsec() {\n        return timestampUsec;\n    }\n\n    public String getLongId() {\n        String idHex = Long.toHexString(Long.valueOf(id));\n        return \"tag:google.com,2005:reader/item/\" + String.format(\"%0\" + (16 - idHex.length()) + \"d\", 0) + idHex;\n        // String.format(\"%0\"+length+\"d\", arr) 中\n        // (16 - id.length())代表的是格式化后字符串的总长度。\n        // d是个占位符，会被arr所替换。arr必须是数字\n        // 0是在arr转化为字符后，长度达不到length的时候，前面以0 不足。\n        // 不能写出 \"tag:google.com,2005:reader/item/\" + String.format(\"%0\" + 16 + \"d\", id)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/LoginResult.java",
    "content": "package me.wizos.loread.bean.inoreader;\n\nimport android.text.TextUtils;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\n\n/**\n * Created by Wizos on 2019/3/27.\n */\n\npublic class LoginResult {\n    public boolean success = false;\n\n    private String error;\n    private String auth;\n\n    public LoginResult(String result) {\n        if (TextUtils.isEmpty(result)) {\n            return;\n        }\n\n        error = App.i().getString(R.string.wrong_unknown);\n\n        String[] info = result.split(\"\\n\");\n        if (info.length == 0) {\n            return;\n        }\n\n        if (info[0].startsWith(\"Error=\")) {\n            error = info[0].replace(\"Error=\", \"\");\n            if (error.toLowerCase().equals(\"wrong_username_or_password\")) {\n                error = App.i().getString(R.string.wrong_username_or_password);\n            }\n        } else {\n            for (String tmp : info) {\n                if (tmp.startsWith(\"Auth=\")) {\n                    success = true;\n                    auth = \"GoogleLogin \" + tmp;\n                }\n            }\n        }\n    }\n\n    public String getError() {\n        return error;\n    }\n\n    public void setError(String error) {\n        this.error = error;\n    }\n\n    public String getAuth() {\n        return auth;\n    }\n\n    public void setAuth(String auth) {\n        this.auth = auth;\n    }\n\n    @Override\n    public String toString() {\n        return \"LoginResult{\" +\n                \"error='\" + error + '\\'' +\n                \", auth='\" + auth + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/Readability.java",
    "content": "package me.wizos.loread.bean.inoreader;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport org.parceler.Parcel;\n\n/**\n * 调用 Mercy 接口的返回实体\n * Created by Wizos on 2017/12/17.\n */\n\n@Parcel\npublic class Readability {\n    @SerializedName(\"title\")\n    String title;\n\n    @SerializedName(\"author\")\n    String author;\n\n    @SerializedName(\"date_published\")\n    String date_published;\n\n    @SerializedName(\"dek\")\n    String dek;\n\n    @SerializedName(\"lead_image_url\")\n    String lead_image_url;\n\n    @SerializedName(\"content\")\n    String content;\n\n    @SerializedName(\"next_page_url\")\n    String next_page_url;\n\n    @SerializedName(\"url\")\n    String url;\n\n    @SerializedName(\"domain\")\n    String domain;\n\n    @SerializedName(\"excerpt\")\n    String excerpt;\n\n    @SerializedName(\"word_count\")\n    int word_count;\n\n    @SerializedName(\"direction\")\n    String direction;\n\n    @SerializedName(\"total_pages\")\n    int total_pages;\n\n    @SerializedName(\"rendered_pages\")\n    int rendered_pages;\n\n    public String getTitle() {\n        return title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    public String getAuthor() {\n        return author;\n    }\n\n    public void setAuthor(String author) {\n        this.author = author;\n    }\n\n    public String getDate_published() {\n        return date_published;\n    }\n\n    public void setDate_published(String date_published) {\n        this.date_published = date_published;\n    }\n\n    public String getDek() {\n        return dek;\n    }\n\n    public void setDek(String dek) {\n        this.dek = dek;\n    }\n\n    public String getLead_image_url() {\n        return lead_image_url;\n    }\n\n    public void setLead_image_url(String lead_image_url) {\n        this.lead_image_url = lead_image_url;\n    }\n\n    public String getContent() {\n        return content;\n    }\n\n    public void setContent(String content) {\n        this.content = content;\n    }\n\n    public String getNext_page_url() {\n        return next_page_url;\n    }\n\n    public void setNext_page_url(String next_page_url) {\n        this.next_page_url = next_page_url;\n    }\n\n    public String getUrl() {\n        return url;\n    }\n\n    public void setUrl(String url) {\n        this.url = url;\n    }\n\n    public String getDomain() {\n        return domain;\n    }\n\n    public void setDomain(String domain) {\n        this.domain = domain;\n    }\n\n    public String getExcerpt() {\n        return excerpt;\n    }\n\n    public void setExcerpt(String excerpt) {\n        this.excerpt = excerpt;\n    }\n\n    public int getWord_count() {\n        return word_count;\n    }\n\n    public void setWord_count(int word_count) {\n        this.word_count = word_count;\n    }\n\n    public String getDirection() {\n        return direction;\n    }\n\n    public void setDirection(String direction) {\n        this.direction = direction;\n    }\n\n    public int getTotal_pages() {\n        return total_pages;\n    }\n\n    public void setTotal_pages(int total_pages) {\n        this.total_pages = total_pages;\n    }\n\n    public int getRendered_pages() {\n        return rendered_pages;\n    }\n\n    public void setRendered_pages(int rendered_pages) {\n        this.rendered_pages = rendered_pages;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/StreamContents.java",
    "content": "package me.wizos.loread.bean.inoreader;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport org.parceler.Parcel;\n\nimport java.util.ArrayList;\n\nimport me.wizos.loread.bean.inoreader.itemContents.Item;\nimport me.wizos.loread.bean.inoreader.itemContents.Self;\n\n\n@Parcel\npublic class StreamContents {\n    @SerializedName(\"id\")\n    String id;\n    @SerializedName(\"title\")\n    String title;\n\n    @SerializedName(\"items\")\n    ArrayList<Item> items;\n\n    @SerializedName(\"continuation\")\n    String continuation;\n\n    @SerializedName(\"updated\")\n    long updated;\n    // ino专用\n    String description;\n    // ino专用\n    @SerializedName(\"updatedUsec\")\n    long updatedUsec;\n    // ino专用\n    @SerializedName(\"self\")\n    Self self;\n    // ino专用\n    @SerializedName(\"direction\")\n    String direction;\n\n    public String getDirection() {\n        return direction;\n    }\n\n    public void setDirection(String direction) {\n        this.direction = direction;\n    }\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getTitle() {\n        return title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    public void setDescription(String description) {\n        this.description = description;\n    }\n\n    public Self getSelf() {\n        return self;\n    }\n\n    public void setSelf(Self self) {\n        this.self = self;\n    }\n\n    public long getUpdated() {\n        return updated;\n    }\n\n    public void setUpdated(long updated) {\n        this.updated = updated;\n    }\n\n    public long getUpdatedUsec() {\n        return updatedUsec;\n    }\n\n    public void setUpdatedUsec(long updatedUsec) {\n        this.updatedUsec = updatedUsec;\n    }\n\n    public ArrayList<Item> getItems() {\n        return items;\n    }\n\n    public void setItems(ArrayList<Item> items) {\n        this.items = items;\n    }\n\n    public String getContinuation() {\n        return continuation;\n    }\n\n    public void setContinuation(String continuation) {\n        this.continuation = continuation;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/StreamPref.java",
    "content": "package me.wizos.loread.bean.inoreader;\n\nimport com.google.gson.annotations.SerializedName;\n\n/**\n * ID 和 Value\n * Created by Wizos on 2016/3/5.\n */\npublic class StreamPref {\n\n    @SerializedName(\"value\")\n    String value;\n\n    @SerializedName(\"id\")\n    String id;\n\n    public String getValue() {\n        return value;\n    }\n\n    public void setValue(String value) {\n        this.value = value;\n    }\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/StreamPrefs.java",
    "content": "package me.wizos.loread.bean.inoreader;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport org.parceler.Parcel;\n\nimport java.util.ArrayList;\nimport java.util.Map;\n\n@Parcel\npublic class StreamPrefs {\n    @SerializedName(\"streamprefs\")\n    Map<String, ArrayList<StreamPref>> streamPrefs;\n\n    public Map<String, ArrayList<StreamPref>> getStreamPrefsMaps() {\n        return streamPrefs;\n    }\n\n    public void setStreamPrefs(Map<String, ArrayList<StreamPref>> streamPrefs) {\n        this.streamPrefs = streamPrefs;\n        System.out.println(\"【StreamPrefs类】\" + streamPrefs);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/SubCategories.java",
    "content": "package me.wizos.loread.bean.inoreader;\n\nimport com.google.gson.annotations.SerializedName;\n\n/**\n * Created by Wizos on 2016/3/11.\n */\npublic class SubCategories {\n    @SerializedName(\"id\")\n    String id;\n\n    @SerializedName(\"label\")\n    String label;\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getLabel() {\n        return label;\n    }\n\n    public void setLabel(String label) {\n        this.label = label;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/Subscription.java",
    "content": "package me.wizos.loread.bean.inoreader;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.ArrayList;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.db.Feed;\n\n/**\n * Created by Wizos on 2016/3/11.\n */\npublic class Subscription {\n    @SerializedName(\"id\")\n    private String id;\n    @SerializedName(\"title\")\n    private String title;\n    @SerializedName(\"categories\")\n    private ArrayList<SubCategories> categories;\n    @SerializedName(\"sortid\")\n    private String sortId;\n    @SerializedName(\"firstitemmsec\")\n    private long firstItemMsec;\n    @SerializedName(\"url\")\n    private String url;\n    @SerializedName(\"htmlUrl\")\n    private String htmlUrl;\n    @SerializedName(\"iconUrl\")\n    private String iconUrl;\n\n\n    public Feed convert2Feed() {\n        Feed feed = new Feed();\n        feed.setId(id);\n        feed.setTitle(title);\n        feed.setFeedUrl(url);\n        feed.setHtmlUrl(htmlUrl);\n        feed.setIconUrl(iconUrl);\n        feed.setDisplayMode(App.OPEN_MODE_RSS);\n        return feed;\n    }\n\n    public String getId() {\n        return id;\n    }\n    public void setId(String id) {\n        this.id = id;\n    }\n    public String getTitle() {\n        return title;\n    }\n    public void setTitle(String title) {\n        this.title = title;\n    }\n    public ArrayList<SubCategories> getCategories() {\n        return categories;\n    }\n    public void setCategories(ArrayList<SubCategories> categories) {\n        this.categories = categories;\n    }\n    public String getSortId() {\n        return sortId;\n    }\n    public void setSortId(String sortId) {\n        this.sortId = sortId;\n    }\n    public long getFirstItemMsec() {\n        return firstItemMsec;\n    }\n    public void setFirstItemMsec(long firstItemMsec) {\n        this.firstItemMsec = firstItemMsec;\n    }\n    public String getUrl() {\n        return url;\n    }\n    public void setUrl(String url) {\n        this.url = url;\n    }\n    public String getHtmlUrl() {\n        return htmlUrl;\n    }\n    public void setHtmlUrl(String htmlUrl) {\n        this.htmlUrl = htmlUrl;\n    }\n    public String getIconUrl() {\n        return iconUrl;\n    }\n    public void setIconUrl(String iconUrl) {\n        this.iconUrl = iconUrl;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/Subscriptions.java",
    "content": "package me.wizos.loread.bean.inoreader;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.ArrayList;\n\n/**\n * Created by Wizos on 2016/3/11.\n */\npublic class Subscriptions {\n    @SerializedName(\"subscriptions\")\n    private ArrayList<Subscription> subscriptions;\n    public ArrayList<Subscription> getSubscriptions() {\n        return subscriptions;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/UnreadCounts.java",
    "content": "package me.wizos.loread.bean.inoreader;\n\nimport com.google.gson.annotations.SerializedName;\n\n/**\n * Created by Wizos on 2016/3/11.\n */\npublic class UnreadCounts {\n\n    @SerializedName(\"id\")\n    String id;\n\n    @SerializedName(\"count\")\n    int count;\n\n    @SerializedName(\"newestItemTimestampUsec\")\n    long newestItemTimestampUsec;\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public int getCount() {\n        return count;\n    }\n\n    public void setCount(int count) {\n        this.count = count;\n    }\n\n    public long getNewestItemTimestampUsec() {\n        return newestItemTimestampUsec;\n    }\n\n    public void setNewestItemTimestampUsec(long newestItemTimestampUsec) {\n        this.newestItemTimestampUsec = newestItemTimestampUsec;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/UserInfo.java",
    "content": "package me.wizos.loread.bean.inoreader;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport org.parceler.Parcel;\n\nimport me.wizos.loread.Contract;\nimport me.wizos.loread.db.User;\n\n@Parcel\npublic class UserInfo {\n    @SerializedName(\"userId\")\n    long userId;\n    @SerializedName(\"userProfileId\")\n    String userProfileId;\n\n    @SerializedName(\"userName\")\n    String userName;\n\n    @SerializedName(\"userEmail\")\n    String userEmail;\n\n    @SerializedName(\"isBloggerUser\")\n    Boolean isBloggerUser;\n\n    @SerializedName(\"signupTimeSec\")\n    long signupTimeSec;\n\n    @SerializedName(\"isMultiLoginEnabled\")\n    Boolean isMultiLoginEnabled;\n\n    public long getUserId() {\n        return userId;\n    }\n\n    public void setUserId(long userId) {\n        this.userId = userId;\n    }\n\n    public String getUserName() {\n        return userName;\n    }\n\n    public void setUserName(String userName) {\n        this.userName = userName;\n    }\n\n    public String getUserProfileId() {\n        return userProfileId;\n    }\n\n    public void setUserProfileId(String userProfileId) {\n        this.userProfileId = userProfileId;\n    }\n\n    public String getUserEmail() {\n        return userEmail;\n    }\n\n    public void setUserEmail(String userEmail) {\n        this.userEmail = userEmail;\n    }\n\n    public Boolean getIsBloggerUser() {\n        return isBloggerUser;\n    }\n\n    public void setIsBloggerUser(Boolean isBloggerUser) {\n        this.isBloggerUser = isBloggerUser;\n    }\n\n    public long getSignupTimeSec() {\n        return signupTimeSec;\n    }\n\n    public void setSignupTimeSec(long signupTimeSec) {\n        this.signupTimeSec = signupTimeSec;\n    }\n\n    public Boolean getIsMultiLoginEnabled() {\n        return isMultiLoginEnabled;\n    }\n\n    public void setIsMultiLoginEnabled(Boolean isMultiLoginEnabled) {\n        this.isMultiLoginEnabled = isMultiLoginEnabled;\n    }\n\n    public User getUser() {\n        User user = new User();\n        user.setSource(Contract.PROVIDER_INOREADER);\n        user.setId(Contract.PROVIDER_INOREADER + \"_\" + userId);\n        user.setUserId(userId + \"\");\n        user.setUserEmail(userEmail);\n        user.setUserName(userName);\n        return user;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/itemContents/Item.java",
    "content": "package me.wizos.loread.bean.inoreader.itemContents;\n\nimport java.util.ArrayList;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.bean.Enclosure;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.network.api.BaseApi;\nimport me.wizos.loread.utils.ArticleUtil;\n\n/**\n * Stream content 和 Item content （貌似已被官方弃用）两个api返回指内的文章项\n * Created by Wizos on 2016/3/11.\n */\npublic class Item {\n    private String id;\n    private String title;\n    private long published;\n    private long updated;\n    private long crawlTimeMsec;\n    private long timestampUsec;\n    private ArrayList<String> categories;\n\n    private long starred; // 加星的时间\n    private ArrayList<Enclosure> enclosure; // 附件：这个还不知道是什么用处，不过可以显示图片\n    private ArrayList<Enclosure> canonical;\n    private ArrayList<Enclosure> alternate;\n    private Summary summary;\n    private String author;\n    private Origin origin;\n\n//这应该是开启了社交后才会有的字段\n//            \"likingUsers\": [],\n//             \"comments\": [],\n//             \"commentsNum\": -1,\n//             \"annotations\": [],\n\n\n    public long getCrawlTimeMsec() {\n        return crawlTimeMsec;\n    }\n\n    public void setCrawlTimeMsec(long crawlTimeMsec) {\n        this.crawlTimeMsec = crawlTimeMsec;\n    }\n\n    public long getTimestampUsec() {\n        return timestampUsec;\n    }\n\n    public void setTimestampUsec(long timestampUsec) {\n        this.timestampUsec = timestampUsec;\n    }\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public ArrayList<String> getCategories() {\n        return categories;\n    }\n\n    public void setCategories(ArrayList<String> categories) {\n        this.categories = categories;\n    }\n\n    public String getTitle() {\n        return title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    public long getPublished() {\n        return published;\n    }\n\n    public void setPublished(long published) {\n        this.published = published;\n    }\n\n    public long getUpdated() {\n        return updated;\n    }\n\n    public void setUpdated(long updated) {\n        this.updated = updated;\n    }\n\n    public long getStarred() {\n        return starred;\n    }\n\n    public void setStarred(long starred) {\n        this.starred = starred;\n    }\n\n    public ArrayList<Enclosure> getEnclosure() {\n        return enclosure;\n    }\n\n    public void setEnclosure(ArrayList<Enclosure> enclosure) {\n        this.enclosure = enclosure;\n    }\n\n    public ArrayList<Enclosure> getCanonical() {\n        return canonical;\n    }\n\n    public void setCanonical(ArrayList<Enclosure> canonical) {\n        this.canonical = canonical;\n    }\n\n    public ArrayList<Enclosure> getAlternate() {\n        return alternate;\n    }\n\n    public void setAlternate(ArrayList<Enclosure> alternate) {\n        this.alternate = alternate;\n    }\n\n    public Summary getSummary() {\n        return summary;\n    }\n\n    public void setSummary(Summary summary) {\n        this.summary = summary;\n    }\n\n    public String getAuthor() {\n        return author;\n    }\n\n    public void setAuthor(String author) {\n        this.author = author;\n    }\n\n    public Origin getOrigin() {\n        return origin;\n    }\n\n    public void setOrigin(Origin origin) {\n        this.origin = origin;\n    }\n\n\n    public Article convert(BaseApi.ArticleChanger articleChanger) {\n        String tempTitle;\n        Article article = new Article();\n        // 返回的字段\n        article.setId(id);\n\n        title = ArticleUtil.getOptimizedTitle(title);\n        article.setTitle(title);\n\n        article.setAuthor(author);\n        article.setPubDate(published * 1000);\n\n        if (canonical != null && canonical.size() > 0) {\n            article.setLink(canonical.get(0).getHref());\n        }\n        if (origin != null) {\n            article.setFeedId(origin.getStreamId());\n            article.setFeedTitle(origin.getTitle());\n        }\n\n        String tmpContent = ArticleUtil.getOptimizedContent(article.getLink(), summary.getContent());\n        tmpContent = ArticleUtil.getOptimizedContentWithEnclosures(tmpContent,enclosure);\n        article.setContent(tmpContent);\n\n        String tmpSummary = ArticleUtil.getOptimizedSummary(tmpContent);\n        article.setSummary(tmpSummary);\n\n        String coverUrl = ArticleUtil.getCoverUrl(article.getLink(),tmpContent);\n        article.setImage(coverUrl);\n\n        // 自己设置的字段\n        // KLog.i(\"【增加文章】\" + article.getId());\n        article.setSaveStatus(App.STATUS_NOT_FILED);\n        if (articleChanger != null) {\n            articleChanger.change(article);\n        }\n        return article;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/itemContents/Origin.java",
    "content": "package me.wizos.loread.bean.inoreader.itemContents;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport org.parceler.Parcel;\n\n@Parcel\npublic class Origin {\n    @SerializedName(\"streamId\")\n    String streamId;\n\n    @SerializedName(\"title\")\n    String title;\n\n    @SerializedName(\"htmlUrl\")\n    String htmlUrl;\n\n\n    public String getHtmlUrl() {\n        return htmlUrl;\n    }\n\n    public void setHtmlUrl(String htmlUrl) {\n        this.htmlUrl = htmlUrl;\n    }\n\n    public String getStreamId() {\n        return streamId;\n    }\n\n    public void setStreamId(String streamId) {\n        this.streamId = streamId;\n    }\n\n    public String getTitle() {\n        return title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    public String toString() {\n        return \"{\\\"streamId\\\": \\\"\" + streamId + \"\\\",\\\"title\\\": \\\"\" + title + \"\\\",\\\"htmlUrl\\\": \\\"\" + htmlUrl + \"\\\"}\";\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/itemContents/Self.java",
    "content": "package me.wizos.loread.bean.inoreader.itemContents;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport org.parceler.Parcel;\n\n@Parcel\npublic class Self {\n    @SerializedName(\"href\")\n    String href;\n\n    public String getHref() {\n        return href;\n    }\n\n    public void setHref(String href) {\n        this.href = href;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/inoreader/itemContents/Summary.java",
    "content": "package me.wizos.loread.bean.inoreader.itemContents;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport org.parceler.Parcel;\n\n@Parcel\npublic class Summary {\n    @SerializedName(\"direction\")\n    String direction;\n\n    @SerializedName(\"content\")\n    String content;\n\n    public String getContent() {\n        return content;\n    }\n\n    public void setContent(String content) {\n        this.content = content;\n    }\n\n    public String getDirection() {\n        return direction;\n    }\n\n    public void setDirection(String direction) {\n        this.direction = direction;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/loread/LoginParam.java",
    "content": "//package me.wizos.loread.bean.loread;\n//\n//public class LoginParam extends RequestJsonBody{\n//    private String user = \"admin\";\n//    private String password;\n//    private String op = \"login\";\n//\n//    public String getUser() {\n//        return user;\n//    }\n//\n//    public void setUser(String user) {\n//        this.user = user;\n//    }\n//\n//    public String getPassword() {\n//        return password;\n//    }\n//\n//    public void setPassword(String password) {\n//        this.password = password;\n//    }\n//\n//    @Override\n//    public String toString() {\n//        return \"login{\" +\n//                \"user='\" + user + '\\'' +\n//                \", password='\" + password + '\\'' +\n//                \", op='\" + op + '\\'' +\n//                '}';\n//    }\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/loread/RequestJsonBody.java",
    "content": "//package me.wizos.loread.bean.loread;\n//\n//public class RequestJsonBody {\n//    private String op;\n//    private String sid;\n//\n//    public void setSid(String sid) {\n//        this.sid = sid;\n//    }\n//    public String getSid() {\n//        return sid;\n//    }\n//\n//    public String getOp() {\n//        return op;\n//    }\n//\n//    public void setOp(String op) {\n//        this.op = op;\n//    }\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/loread/Response.java",
    "content": "//package me.wizos.loread.bean.loread;\n//\n//import com.google.gson.annotations.SerializedName;\n//import com.socks.library.KLog;\n//\n//public class Response<T> {\n//    private int code;\n//    @SerializedName(value = \"msg\", alternate = {\"error\"})\n//    private String msg;\n//    private T data;\n//\n//    public boolean isSuccessful() {\n//        if (code == 0) {\n//            KLog.i(\"请求正常\");\n//            return true;\n//        }\n//        KLog.i(\"请求异常：\" + data);\n//        return false;\n//    }\n//\n//    public int getCode() {\n//        return code;\n//    }\n//\n//    public void setCode(int code) {\n//        this.code = code;\n//    }\n//\n//    public T getData() {\n//        return data;\n//    }\n//\n//    public void setData(T data) {\n//        this.data = data;\n//    }\n//\n//    public String getMsg() {\n//        return msg;\n//    }\n//\n//    public void setMsg(String msg) {\n//        this.msg = msg;\n//    }\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/proxynode/AnonymityLevel.java",
    "content": "package me.wizos.loread.bean.proxynode;\n\nimport me.wizos.loread.gson.GsonEnum;\n\npublic enum AnonymityLevel implements GsonEnum<AnonymityLevel> {\n//    TRANSPARENT, //\"透明代理\"\n//    ANONYMOUS, // \"匿名代理\"\n//    DISTORTING, // \"欺骗性代理\"\n//    ELITE, // \"高匿代理\"\n//    UNKNOW //\"未知代理\"\n\n    TRANSPARENT(\"TRANSPARENT\"), ANONYMOUS(\"ANONYMOUS\"), DISTORTING(\"DISTORTING\"), ELITE(\"ELITE\"), UNKNOW(\"UNKNOW\");\n    private final String anonymityLevel;\n    AnonymityLevel(String anonymityLevel) {\n        this.anonymityLevel = anonymityLevel;\n    }\n\n\n    public String getAnonymityLevel() {\n        return anonymityLevel;\n    }\n\n    public static AnonymityLevel parse(String level) {\n        switch (level) {\n            case \"TRANSPARENT\":\n                return AnonymityLevel.TRANSPARENT;\n            case \"ANONYMOUS\":\n                return AnonymityLevel.ANONYMOUS;\n            case \"DISTORTING\":\n                return AnonymityLevel.DISTORTING;\n            case \"ELITE\":\n                return AnonymityLevel.ELITE;\n            case \"UNKNOW\":\n                return AnonymityLevel.UNKNOW;\n            default:\n                throw new IllegalArgumentException(\"There is not enum names with [\" + level + \"] of type AnonymityLevel exists! \");\n        }\n    }\n\n    @Override\n    public AnonymityLevel deserialize(String jsonEnum) {\n        return AnonymityLevel.parse(jsonEnum);\n    }\n\n    @Override\n    public String serialize() {\n        return this.getAnonymityLevel();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/proxynode/ProxyNode.java",
    "content": "package me.wizos.loread.bean.proxynode;\n\nimport java.io.Serializable;\nimport java.net.Proxy;\n\n/**\n * 代理节点并不能被用于翻墙，因为会被“GFW”给发现，并重置连接。\n * 只能用于国内反爬虫\n */\npublic class ProxyNode implements Serializable {\n    public ProxyType type;\n    public AnonymityLevel level;\n    public String hostname;\n    public int port;\n\n    public ProxyNode() {\n    }\n\n    public ProxyNode(ProxyType type, AnonymityLevel level, String hostname, int port) {\n        this.type = type;\n        this.level = level;\n        this.hostname = hostname;\n        this.port = port;\n    }\n\n    @Override\n    public String toString() {\n        return \"ProxyConfig{\" +\n                \"type=\" + type +\n                \", level=\" + level +\n                \", hostname='\" + hostname + '\\'' +\n                \", port=\" + port +\n                '}';\n    }\n\n\n    public Proxy.Type getProxyType() {\n        switch (type) {\n            case DIRECT:\n                return Proxy.Type.DIRECT;\n            case HTTP:\n                return Proxy.Type.HTTP;\n            case SOCKS:\n                return Proxy.Type.SOCKS;\n            default:\n                throw new IllegalArgumentException(\"There is not enum names with [\" + level + \"] of type AnonymityLevel exists! \");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/proxynode/ProxyType.java",
    "content": "package me.wizos.loread.bean.proxynode;\n\nimport me.wizos.loread.gson.GsonEnum;\n\npublic enum ProxyType implements GsonEnum<ProxyType> {\n    DIRECT(\"DIRECT\"), HTTP(\"HTTP\"), SOCKS(\"SOCKS\");\n    private final String proxyType;\n    ProxyType(String proxyType) {\n        this.proxyType = proxyType;\n    }\n\n\n    public String getProxyType() {\n        return proxyType;\n    }\n\n    public static ProxyType parse(String type) {\n        switch (type) {\n            case \"DIRECT\":\n                return ProxyType.DIRECT;\n            case \"HTTP\":\n                return ProxyType.HTTP;\n            case \"SOCKS\":\n                return ProxyType.SOCKS;\n            default:\n                throw new IllegalArgumentException(\"There is not enum names with [\" + type + \"] of type AnonymityLevel exists! \");\n        }\n    }\n\n    @Override\n    public ProxyType deserialize(String jsonEnum) {\n        return ProxyType.parse(jsonEnum);\n    }\n\n    @Override\n    public String serialize() {\n        return this.getProxyType();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/search/QuickAdd.java",
    "content": "//package me.wizos.loreadx.bean.search;\n//\n//import com.google.gson.annotations.SerializedName;\n//\n///**\n// * Created by Wizos on 2018/1/2.\n// */\n//\n//public class QuickAdd {\n//    @SerializedName(\"streamId\")\n//    String streamId;\n//    @SerializedName(\"streamName\")\n//    String streamName;\n//    @SerializedName(\"query\")\n//    String query;\n//    @SerializedName(\"numResults\") // 如果Feed由于某种原因未被添加，则numResults将为0。 即使用户已订阅，添加订阅源时也会为1。\n//            int numResults;\n//\n//    public String getStreamId() {\n//        return streamId;\n//    }\n//\n//    public void setStreamId(String streamId) {\n//        this.streamId = streamId;\n//    }\n//\n//    public String getStreamName() {\n//        return streamName;\n//    }\n//\n//    public void setStreamName(String streamName) {\n//        this.streamName = streamName;\n//    }\n//\n//    public String getQuery() {\n//        return query;\n//    }\n//\n//    public void setQuery(String query) {\n//        this.query = query;\n//    }\n//\n//    public int getNumResults() {\n//        return numResults;\n//    }\n//\n//    public void setNumResults(int numResults) {\n//        this.numResults = numResults;\n//    }\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/search/SearchFeedItem.java",
    "content": "package me.wizos.loread.bean.search;\n\nimport me.wizos.loread.bean.feedly.FeedItem;\n\n/**\n * Created by Wizos on 2017/12/31.\n */\n\npublic class SearchFeedItem extends FeedItem {\n    private long lastUpdated; // 可能用不到吧，和 Updated 字段类似\n    private float score;\n    private float coverage;\n    private float coverageScore;\n    private float averageReadTime;\n    private String websiteTitle;\n    // private int totalTagCount;\n    // private ArrayList<> tagCounts;\n    // private ArrayList<String> deliciousTags;\n\n    public long getLastUpdated() {\n        return lastUpdated;\n    }\n\n    public void setLastUpdated(long lastUpdated) {\n        this.lastUpdated = lastUpdated;\n    }\n\n    public float getScore() {\n        return score;\n    }\n\n    public void setScore(float score) {\n        this.score = score;\n    }\n\n    public float getCoverage() {\n        return coverage;\n    }\n\n    public void setCoverage(float coverage) {\n        this.coverage = coverage;\n    }\n\n    public float getCoverageScore() {\n        return coverageScore;\n    }\n\n    public void setCoverageScore(float coverageScore) {\n        this.coverageScore = coverageScore;\n    }\n\n    public float getAverageReadTime() {\n        return averageReadTime;\n    }\n\n    public void setAverageReadTime(float averageReadTime) {\n        this.averageReadTime = averageReadTime;\n    }\n\n    public String getWebsiteTitle() {\n        return websiteTitle;\n    }\n\n    public void setWebsiteTitle(String websiteTitle) {\n        this.websiteTitle = websiteTitle;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/search/SearchFeeds.java",
    "content": "package me.wizos.loread.bean.search;\n\nimport java.util.ArrayList;\n\n/**\n * Created by Wizos on 2017/12/31.\n */\n\npublic class SearchFeeds {\n    private String hint;\n    private ArrayList<String> related;\n    private ArrayList<SearchFeedItem> results;\n    private String queryType;\n    private String scheme;\n\n    public String getHint() {\n        return hint;\n    }\n\n    public void setHint(String hint) {\n        this.hint = hint;\n    }\n\n    public String getQueryType() {\n        return queryType;\n    }\n\n    public void setQueryType(String queryType) {\n        this.queryType = queryType;\n    }\n\n    public String getScheme() {\n        return scheme;\n    }\n\n    public void setScheme(String scheme) {\n        this.scheme = scheme;\n    }\n\n    public ArrayList<String> getRelated() {\n        return related;\n    }\n\n    public void setRelated(ArrayList<String> related) {\n        this.related = related;\n    }\n\n    public ArrayList<SearchFeedItem> getResults() {\n        return results;\n    }\n\n    public void setResults(ArrayList<SearchFeedItem> results) {\n        this.results = results;\n    }\n\n    @Override\n    public String toString() {\n        return \"SearchFeeds{\" +\n                \"hint='\" + hint + '\\'' +\n                \", related=\" + related +\n                \", results=\" + results +\n                \", queryType='\" + queryType + '\\'' +\n                \", scheme='\" + scheme + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/search/StreamFeed.java",
    "content": "//package me.wizos.loreadx.bean.search;\n//\n//import com.google.gson.annotations.SerializedName;\n//\n///**\n// * Created by Wizos on 2017/12/31.\n// */\n//\n//public class StreamFeed {\n//    @SerializedName(\"title\")\n//    String title;\n//\n//    @SerializedName(\"feedUrl\")\n//    String feedUrl;\n//\n//    @SerializedName(\"iconUrl\")\n//    String iconUrl;\n//\n//    @SerializedName(\"description\")\n//    String description;\n//\n//    @SerializedName(\"subscribers\")\n//    int subscribers;\n//\n//    @SerializedName(\"iconUrl\")\n//    int articlesPerWeek;\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/request/GetArticles.java",
    "content": "package me.wizos.loread.bean.ttrss.request;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.HashSet;\nimport java.util.List;\n\nimport me.wizos.loread.utils.StringUtils;\n\npublic class GetArticles {\n    private String sid;\n    private String op = \"getArticle\";\n    @SerializedName(\"article_id\")\n    private String articleIds;\n\n    public GetArticles(String sid) {\n        this.sid = sid;\n    }\n\n    public void setArticleIds(String articleIds) {\n        this.articleIds = articleIds;\n    }\n    public void setArticleIds(HashSet<String> articleIdSet) {\n        this.articleIds = StringUtils.join(\",\",articleIdSet);\n    }\n    public void setArticleIds(List<String> articleIdList) {\n        this.articleIds = StringUtils.join(\",\",articleIdList);\n    }\n    public void setSid(String sid) {\n        this.sid = sid;\n    }\n    public String getSid() {\n        return sid;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/request/GetCategories.java",
    "content": "package me.wizos.loread.bean.ttrss.request;\n\npublic class GetCategories {\n    private String sid;\n    private String op = \"getCategories\";\n    private boolean unread_only = false;\n    private boolean include_empty = true;\n\n    public GetCategories(String sid) {\n        this.sid = sid;\n    }\n\n    public String getSid() {\n        return sid;\n    }\n\n    public void setSid(String sid) {\n        this.sid = sid;\n    }\n\n    public boolean isUnread_only() {\n        return unread_only;\n    }\n\n    public void setUnread_only(boolean unread_only) {\n        this.unread_only = unread_only;\n    }\n\n    public boolean isInclude_empty() {\n        return include_empty;\n    }\n\n    public void setInclude_empty(boolean include_empty) {\n        this.include_empty = include_empty;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/request/GetFeeds.java",
    "content": "package me.wizos.loread.bean.ttrss.request;\n\npublic class GetFeeds {\n    private String sid;\n    private String op = \"getFeeds\";\n\n    public GetFeeds(String sid) {\n        this.sid = sid;\n    }\n\n    /**\n     * 0 Uncategorized\n     * -1 Special (e.g. Starred, Published, Archived, etc.)\n     * -2 Labels\n     * -3 All feeds, excluding virtual feeds (e.g. Labels and such)\n     * -4 All feeds, including virtual feeds\n     */\n    private int cat_id = -3;\n    private boolean unread_only = false;\n\n\n    public String getSid() {\n        return sid;\n    }\n\n    public void setSid(String sid) {\n        this.sid = sid;\n    }\n\n    public int getCat_id() {\n        return cat_id;\n    }\n\n    public void setCat_id(int cat_id) {\n        this.cat_id = cat_id;\n    }\n\n    public boolean isUnread_only() {\n        return unread_only;\n    }\n\n    public void setUnread_only(boolean unread_only) {\n        this.unread_only = unread_only;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/request/GetHeadlines.java",
    "content": "package me.wizos.loread.bean.ttrss.request;\n\npublic class GetHeadlines {\n    private String op = \"getHeadlines\";\n    private String sid;\n    /**\n     * all_articles, unread, adaptive, marked, updated\n     */\n    private String view_mode = \"unread\";\n    /**\n     * -1 starred\n     * -2 published\n     * -3 fresh\n     * -4 all articles\n     * 0 - archived\n     * IDs < -10 labels\n     */\n    private String feed_id = \"-4\";\n    /**\n     * date_reverse - oldest first\n     * feed_dates - newest first, goes by feed date\n     * (nothing) - default\n     */\n    private String order_by = \"date_reverse\";\n\n    private int limit = 50;\n    private int skip;\n    private String since_id;\n    private boolean is_cat = false;\n\n    private boolean show_content = true;\n    private boolean include_attachments = true;\n    private boolean has_sandbox = true;\n\n\n\n    public String getOp() {\n        return op;\n    }\n\n    public void setOp(String op) {\n        this.op = op;\n    }\n\n    public String getSid() {\n        return sid;\n    }\n\n    public void setSid(String sid) {\n        this.sid = sid;\n    }\n\n    public String getView_mode() {\n        return view_mode;\n    }\n\n    public void setView_mode(String view_mode) {\n        this.view_mode = view_mode;\n    }\n\n    public String getFeed_id() {\n        return feed_id;\n    }\n\n    public void setFeed_id(String feed_id) {\n        this.feed_id = feed_id;\n    }\n\n    public String getOrder_by() {\n        return order_by;\n    }\n\n    public void setOrder_by(String order_by) {\n        this.order_by = order_by;\n    }\n\n    public int getLimit() {\n        return limit;\n    }\n\n    public void setLimit(int limit) {\n        this.limit = limit;\n    }\n\n    public int getSkip() {\n        return skip;\n    }\n\n    public void setSkip(int skip) {\n        this.skip = skip;\n    }\n\n    public String getSince_id() {\n        return since_id;\n    }\n\n    public void setSince_id(String since_id) {\n        this.since_id = since_id;\n    }\n\n    public boolean isIs_cat() {\n        return is_cat;\n    }\n\n    public void setIs_cat(boolean is_cat) {\n        this.is_cat = is_cat;\n    }\n\n    public boolean isShow_content() {\n        return show_content;\n    }\n\n    public void setShow_content(boolean show_content) {\n        this.show_content = show_content;\n    }\n\n    public boolean isInclude_attachments() {\n        return include_attachments;\n    }\n\n    public void setInclude_attachments(boolean include_attachments) {\n        this.include_attachments = include_attachments;\n    }\n\n    public boolean isHas_sandbox() {\n        return has_sandbox;\n    }\n\n    public void setHas_sandbox(boolean has_sandbox) {\n        this.has_sandbox = has_sandbox;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/request/GetSavedItemIds.java",
    "content": "package me.wizos.loread.bean.ttrss.request;\n\npublic class GetSavedItemIds {\n    private String sid;\n    private String op = \"getSavedItemIds\";\n\n    public GetSavedItemIds(String sid) {\n        this.sid = sid;\n    }\n\n    public void setSid(String sid) {\n        this.sid = sid;\n    }\n    public String getSid() {\n        return sid;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/request/GetUnreadItemIds.java",
    "content": "package me.wizos.loread.bean.ttrss.request;\n\npublic class GetUnreadItemIds {\n    private String sid;\n    private String op = \"getUnreadItemIds\";\n\n    public GetUnreadItemIds(String sid) {\n        this.sid = sid;\n    }\n\n    public void setSid(String sid) {\n        this.sid = sid;\n    }\n    public String getSid() {\n        return sid;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/request/LoginParam.java",
    "content": "package me.wizos.loread.bean.ttrss.request;\n\npublic class LoginParam {\n    private String user = \"admin\";\n    private String password;\n    private String op = \"login\";\n\n    public String getUser() {\n        return user;\n    }\n\n    public void setUser(String user) {\n        this.user = user;\n    }\n\n    public String getPassword() {\n        return password;\n    }\n\n    public void setPassword(String password) {\n        this.password = password;\n    }\n\n    @Override\n    public String toString() {\n        return \"login{\" +\n                \"user='\" + user + '\\'' +\n                \", password='\" + password + '\\'' +\n                \", op='\" + op + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/request/RequestParam.java",
    "content": "package me.wizos.loread.bean.ttrss.request;\n\npublic class RequestParam {\n    private String op;\n    private String sid;\n\n    public void setSid(String sid) {\n        this.sid = sid;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/request/SearchHeadlines.java",
    "content": "package me.wizos.loread.bean.ttrss.request;\n\npublic class SearchHeadlines {\n    private String op = \"getHeadlines\";\n    private String sid;\n    private String view_mode = \"all_articles\";\n\n    private String search_mode = \"all_feeds\";\n    // 搜索词\n    private String search;\n\n    /**\n     * -1 starred\n     * -2 published\n     * -3 fresh\n     * -4 all articles\n     * 0 - archived\n     * IDs < -10 labels\n     */\n    private String feed_id = \"-4\";\n    /**\n     * date_reverse - oldest first\n     * feed_dates - newest first, goes by feed date\n     * (nothing) - default\n     */\n    private String order_by = \"feed_dates\";\n\n    private int limit = 50;\n    private int skip;\n    private String since_id;\n    private boolean is_cat = false;\n\n    private boolean show_content = true;\n    private boolean include_attachments = true;\n    private boolean has_sandbox = true;\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/request/SearchMode.java",
    "content": "package me.wizos.loread.bean.ttrss.request;\n\npublic enum SearchMode {\n    all_feeds,\n    this_feed,\n    //(category containing requested feed)\n    this_cat\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/request/SubscribeToFeed.java",
    "content": "package me.wizos.loread.bean.ttrss.request;\n\npublic class SubscribeToFeed {\n    private String sid;\n    private String op = \"subscribeToFeed\";\n    private String category_id;\n    private String feed_url;\n\n    public SubscribeToFeed(String sid) {\n        this.sid = sid;\n    }\n\n    public String getSid() {\n        return sid;\n    }\n\n    public void setSid(String sid) {\n        this.sid = sid;\n    }\n\n    public String getCategory_id() {\n        return category_id;\n    }\n\n    public void setCategory_id(String category_id) {\n        this.category_id = category_id;\n    }\n\n    public String getFeed_url() {\n        return feed_url;\n    }\n\n    public void setFeed_url(String feed_url) {\n        this.feed_url = feed_url;\n    }\n\n    @Override\n    public String toString() {\n        return \"SubscribeToFeed{\" +\n                \"op='\" + op + '\\'' +\n                \", sid='\" + sid + '\\'' +\n                \", category_id='\" + category_id + '\\'' +\n                \", feed_url='\" + feed_url + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/request/UnsubscribeFeed.java",
    "content": "package me.wizos.loread.bean.ttrss.request;\n\nimport com.google.gson.annotations.SerializedName;\n\npublic class UnsubscribeFeed {\n    private String sid;\n    private String op = \"unsubscribeFeed\";\n    @SerializedName(\"feed_id\")\n    private int feedId;\n\n    public UnsubscribeFeed(String sid) {\n        this.sid = sid;\n    }\n\n    public String getOp() {\n        return op;\n    }\n\n    public void setOp(String op) {\n        this.op = op;\n    }\n\n    public String getSid() {\n        return sid;\n    }\n\n    public void setSid(String sid) {\n        this.sid = sid;\n    }\n\n    public int getFeedId() {\n        return feedId;\n    }\n\n    public void setFeedId(int feedId) {\n        this.feedId = feedId;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/request/UpdateArticle.java",
    "content": "package me.wizos.loread.bean.ttrss.request;\n\nimport android.text.TextUtils;\n\npublic class UpdateArticle {\n    private String sid;\n    private String op = \"updateArticle\";\n\n    // 如果有多个请用“,”分割\n    private String article_ids;\n\n    // 0 - starred, 1 - published, 2 - unread, 3 - article note\n    private int field;\n\n    // 0 - set to false, 1 - set to true, 2 - toggle\n    private int mode;\n\n    // 设置 note\n    private String data;\n\n\n    public UpdateArticle(String sid) {\n        this.sid = sid;\n    }\n\n    public String getSid() {\n        return sid;\n    }\n\n    public void setSid(String sid) {\n        this.sid = sid;\n    }\n\n    public String getArticle_ids() {\n        return article_ids;\n    }\n\n    public void setArticle_ids(String article_ids) {\n        this.article_ids = article_ids;\n    }\n\n    public void addArticle_id(String article_id) {\n        if (!TextUtils.isEmpty(article_ids)) {\n            article_ids = article_ids + \",\" + article_id;\n        } else {\n            article_ids = article_id;\n        }\n    }\n\n    public int getField() {\n        return field;\n    }\n\n    public void setField(int field) {\n        this.field = field;\n    }\n\n    public int getMode() {\n        return mode;\n    }\n\n    public void setMode(int mode) {\n        this.mode = mode;\n    }\n\n    public String getData() {\n        return data;\n    }\n\n    public void setData(String data) {\n        this.data = data;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/result/ArticleItem.java",
    "content": "package me.wizos.loread.bean.ttrss.result;\n\nimport java.util.List;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.bean.Enclosure;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.network.api.BaseApi;\nimport me.wizos.loread.utils.ArticleUtil;\n\npublic class ArticleItem {\n    private int id;\n    private String guid;\n\n    private boolean unread;\n    private boolean marked;\n    private boolean published;\n\n    private long updated;\n    private boolean is_updated;\n\n    private String title;\n    private String link;\n    private String author;\n    private String content;\n\n    private List<Enclosure> attachments;\n    private List<String> tags;\n    private List<Object> labels;\n    private String comments_link;\n    private int comments_count;\n\n    private String feed_id;\n    private String feed_title;\n\n    private String flavor_image;\n    private String flavor_stream;\n    private String lang = \"zh\";\n    private String note = \"\";\n\n\n    private int score;\n    private boolean always_display_attachments;\n\n\n    public Article convert(BaseApi.ArticleChanger articleChanger) {\n        Article article = new Article();\n        article.setId(String.valueOf(id));\n        title = ArticleUtil.getOptimizedTitle(title);\n        article.setTitle(title);\n\n        article.setAuthor(author);\n        article.setPubDate(updated * 1000);\n\n        article.setLink(link);\n        article.setFeedId(feed_id);\n        article.setFeedTitle(feed_title);\n\n        String tmpContent = ArticleUtil.getOptimizedContent(article.getLink(), content);\n        tmpContent = ArticleUtil.getOptimizedContentWithEnclosures(tmpContent,attachments);\n        article.setContent(tmpContent);\n\n        String tmpSummary = ArticleUtil.getOptimizedSummary(tmpContent);\n        article.setSummary(tmpSummary);\n\n        String coverUrl = ArticleUtil.getCoverUrl(article.getLink(),tmpContent);\n        article.setImage(coverUrl);\n\n        // 自己设置的字段\n        //  KLog.i(\"【增加文章】\" + article.getId());\n        article.setSaveStatus(App.STATUS_NOT_FILED);\n        if (unread) {\n            article.setReadStatus(App.STATUS_UNREAD);\n        } else {\n            article.setReadStatus(App.STATUS_READED);\n        }\n        if (marked) {\n            article.setStarStatus(App.STATUS_STARED);\n        } else {\n            article.setStarStatus(App.STATUS_UNSTAR);\n        }\n\n        if (articleChanger != null) {\n            articleChanger.change(article);\n        }\n        return article;\n    }\n\n    @Override\n    public String toString() {\n        return \"TTRSSArticleItem{\" +\n                \"id=\" + id +\n                \", guid='\" + guid + '\\'' +\n                \", unread=\" + unread +\n                \", marked=\" + marked +\n                \", published=\" + published +\n                \", updated=\" + updated +\n                \", is_updated=\" + is_updated +\n                \", title='\" + title + '\\'' +\n                \", link='\" + link + '\\'' +\n                \", author='\" + author + '\\'' +\n                \", content='\" + content + '\\'' +\n                \", attachments=\" + attachments +\n                \", tags=\" + tags +\n                \", labels=\" + labels +\n                \", comments_link='\" + comments_link + '\\'' +\n                \", comments_count=\" + comments_count +\n                \", feed_id='\" + feed_id + '\\'' +\n                \", feed_title='\" + feed_title + '\\'' +\n                \", flavor_image='\" + flavor_image + '\\'' +\n                \", flavor_stream='\" + flavor_stream + '\\'' +\n                \", lang='\" + lang + '\\'' +\n                \", note='\" + note + '\\'' +\n                \", score=\" + score +\n                \", always_display_attachments=\" + always_display_attachments +\n                '}';\n    }\n\n    public int getId() {\n        return id;\n    }\n\n    public String getGuid() {\n        return guid;\n    }\n\n    public boolean isUnread() {\n        return unread;\n    }\n\n    public boolean isMarked() {\n        return marked;\n    }\n\n    public boolean isPublished() {\n        return published;\n    }\n\n    public long getUpdated() {\n        return updated;\n    }\n\n    public boolean isIs_updated() {\n        return is_updated;\n    }\n\n    public String getTitle() {\n        return title;\n    }\n\n    public String getLink() {\n        return link;\n    }\n\n    public String getAuthor() {\n        return author;\n    }\n\n    public String getContent() {\n        return content;\n    }\n\n    public List<Enclosure> getAttachments() {\n        return attachments;\n    }\n\n    public List<String> getTags() {\n        return tags;\n    }\n\n    public List<Object> getLabels() {\n        return labels;\n    }\n\n    public String getComments_link() {\n        return comments_link;\n    }\n\n    public int getComments_count() {\n        return comments_count;\n    }\n\n    public String getFeed_id() {\n        return feed_id;\n    }\n\n    public String getFeed_title() {\n        return feed_title;\n    }\n\n    public String getFlavor_image() {\n        return flavor_image;\n    }\n\n    public String getFlavor_stream() {\n        return flavor_stream;\n    }\n\n    public String getLang() {\n        return lang;\n    }\n\n    public String getNote() {\n        return note;\n    }\n\n    public int getScore() {\n        return score;\n    }\n\n    public boolean isAlways_display_attachments() {\n        return always_display_attachments;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/result/Attachment.java",
    "content": "//package me.wizos.loreadx.bean.ttrss.result;\n//\n//public class Attachment {\n//    private String content_url;\n//    private String content_type;\n//\n//    public String getContent_url() {\n//        return content_url;\n//    }\n//\n//    public String getContent_type() {\n//        return content_type;\n//    }\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/result/CategoryItem.java",
    "content": "package me.wizos.loread.bean.ttrss.result;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport me.wizos.loread.db.Category;\n\npublic class CategoryItem {\n    @SerializedName(\"id\")\n    private String id;\n    @SerializedName(\"title\")\n    private String title;\n\n    private int unread;\n    private int order_id;\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getTitle() {\n        return title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    public int getUnread() {\n        return unread;\n    }\n\n    public void setUnread(int unread) {\n        this.unread = unread;\n    }\n\n    public int getOrder_id() {\n        return order_id;\n    }\n\n    public void setOrder_id(int order_id) {\n        this.order_id = order_id;\n    }\n\n\n    public Category convert() {\n        Category category = new Category();\n        category.setId(id);\n        category.setTitle(title);\n//        category.setId( \"user/\" + id);\n//        category.setUnreadCount(unread);\n        return category;\n    }\n\n\n    @Override\n    public String toString() {\n        return \"TTRSSCategoryItem{\" +\n                \"id='\" + id + '\\'' +\n                \", title='\" + title + '\\'' +\n                \", unread=\" + unread +\n                \", order_id=\" + order_id +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/result/FeedItem.java",
    "content": "package me.wizos.loread.bean.ttrss.result;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.db.Feed;\n\npublic class FeedItem {\n    @SerializedName(\"id\")\n    private int id;\n    @SerializedName(\"title\")\n    private String title;\n    @SerializedName(\"feed_url\")\n    private String feedUrl;\n    @SerializedName(\"site_url\")\n    private String siteUrl;\n    @SerializedName(\"unread\")\n    private int unread;\n    @SerializedName(\"cat_id\")\n    private int catId;\n    @SerializedName(\"order_id\")\n    private int orderId;\n    @SerializedName(\"last_updated\")\n    private long lastUpdated;\n    @SerializedName(\"has_icon\")\n    private boolean hasIcon;\n\n\n    public String getFeedUrl() {\n        return feedUrl;\n    }\n    public void setFeedUrl(String feedUrl) {\n        this.feedUrl = feedUrl;\n    }\n    public String getSiteUrl() {\n        return siteUrl;\n    }\n    public void setSiteUrl(String siteUrl) {\n        this.siteUrl = siteUrl;\n    }\n    public String getTitle() {\n        return title;\n    }\n    public void setTitle(String title) {\n        this.title = title;\n    }\n    public int getId() {\n        return id;\n    }\n    public void setId(int id) {\n        this.id = id;\n    }\n    public int getUnread() {\n        return unread;\n    }\n    public void setUnread(int unread) {\n        this.unread = unread;\n    }\n    public int getCatId() {\n        return catId;\n    }\n    public void setCatId(int catId) {\n        this.catId = catId;\n    }\n    public int getOrderId() {\n        return orderId;\n    }\n    public void setOrderId(int orderId) {\n        this.orderId = orderId;\n    }\n    public long getLastUpdated() {\n        return lastUpdated;\n    }\n    public void setLastUpdated(long lastUpdated) {\n        this.lastUpdated = lastUpdated;\n    }\n    public boolean isHasIcon() {\n        return hasIcon;\n    }\n    public void setHasIcon(boolean hasIcon) {\n        this.hasIcon = hasIcon;\n    }\n\n\n    public Feed convert2Feed() {\n        Feed feed = new Feed();\n        feed.setId(String.valueOf(id));\n        feed.setTitle(title);\n        feed.setFeedUrl(feedUrl);\n        feed.setHtmlUrl(siteUrl);\n        //feed.setIconUrl(visualUrl);\n        feed.setDisplayMode(App.OPEN_MODE_RSS);\n        return feed;\n    }\n\n    @Override\n    public String toString() {\n        return \"TTRSSFeedItem{\" +\n                \"feed_url='\" + feedUrl + '\\'' +\n                \", site_url='\" + siteUrl + '\\'' +\n                \", title='\" + title + '\\'' +\n                \", id=\" + id +\n                \", unread=\" + unread +\n                \", cat_id=\" + catId +\n                \", order_id=\" + orderId +\n                \", last_updated=\" + lastUpdated +\n                \", has_icon=\" + hasIcon +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/result/SubscribeToFeedResult.java",
    "content": "package me.wizos.loread.bean.ttrss.result;\n\npublic class SubscribeToFeedResult {\n    private int code;\n    private int feed_id;\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/result/TTRSSLoginResult.java",
    "content": "package me.wizos.loread.bean.ttrss.result;\n\npublic class TTRSSLoginResult {\n    private String session_id;\n    private int api_level;\n\n    public String getSession_id() {\n        return session_id;\n    }\n\n    public void setSession_id(String session_id) {\n        this.session_id = session_id;\n    }\n\n    public int getApi_level() {\n        return api_level;\n    }\n\n    public void setApi_level(int api_level) {\n        this.api_level = api_level;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/result/TinyResponse.java",
    "content": "package me.wizos.loread.bean.ttrss.result;\n\nimport com.google.gson.annotations.SerializedName;\n\npublic class TinyResponse<T> {\n    private int seq;\n    private int status;\n    @SerializedName(value = \"msg\", alternate = {\"error\"})\n    private String msg;\n    private T content;\n\n    public boolean isSuccessful() {\n        if (status == 0) {\n            //KLog.i(\"请求正常\");\n            return true;\n        }\n        //KLog.i(\"请求异常：\" + content);\n        return false;\n    }\n\n    public int getSeq() {\n        return seq;\n    }\n\n    public void setSeq(int seq) {\n        this.seq = seq;\n    }\n\n    public int getStatus() {\n        return status;\n    }\n\n    public void setStatus(int status) {\n        this.status = status;\n    }\n\n    public T getContent() {\n        return content;\n    }\n\n    public void setContent(T content) {\n        this.content = content;\n    }\n\n    public String getMsg() {\n        return msg;\n    }\n\n    public void setMsg(String msg) {\n        this.msg = msg;\n    }\n\n\n    @Override\n    public String toString() {\n        return \"TTRSSResponse{\" +\n                \"seq=\" + seq +\n                \", status=\" + status +\n                \", msg='\" + msg + '\\'' +\n                \", content=\" + content +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bean/ttrss/result/UpdateArticleResult.java",
    "content": "package me.wizos.loread.bean.ttrss.result;\n\npublic class UpdateArticleResult {\n    private String status;\n    private int updated;\n\n    public String getStatus() {\n        return status;\n    }\n\n    public void setStatus(String status) {\n        this.status = status;\n    }\n\n    public int getUpdated() {\n        return updated;\n    }\n\n    public void setUpdated(int updated) {\n        this.updated = updated;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/behavior/BottomNavigationBehavior.java",
    "content": "package me.wizos.loread.behavior;\n\nimport android.animation.ObjectAnimator;\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.View;\n\nimport androidx.coordinatorlayout.widget.CoordinatorLayout;\nimport androidx.core.view.ViewCompat;\n\n/**\n * 不需要与toolbar联动，即可隐藏底部菜单\n *\n * @link https://blog.csdn.net/Keepsty/article/details/81740663\n * @date 2019/2/3.\n */\n\npublic class BottomNavigationBehavior extends CoordinatorLayout.Behavior<View> {\n    private ObjectAnimator outAnimator, inAnimator;\n\n    public BottomNavigationBehavior(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    // 垂直滑动\n    @Override\n    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {\n        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;\n    }\n\n\n    @Override\n    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {\n        if (dy > 0) {// 上滑隐藏\n            if (outAnimator == null) {\n                outAnimator = ObjectAnimator.ofFloat(child, \"translationY\", 0, child.getHeight());\n                outAnimator.setDuration(200);\n            }\n            if (!outAnimator.isRunning() && child.getTranslationY() <= 0) {\n                outAnimator.start();\n            }\n        } else if (dy < 0) {// 下滑显示\n            if (inAnimator == null) {\n                inAnimator = ObjectAnimator.ofFloat(child, \"translationY\", child.getHeight(), 0);\n                inAnimator.setDuration(200);\n            }\n            if (!inAnimator.isRunning() && child.getTranslationY() >= child.getHeight()) {\n                inAnimator.start();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/behavior/BottomNavigationViewBehavior.java",
    "content": "/*\n * Copyright (C)  LeonDevLifeLog(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage me.wizos.loread.behavior;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.View;\n\nimport androidx.coordinatorlayout.widget.CoordinatorLayout;\nimport androidx.core.view.ViewCompat;\n\nimport com.google.android.material.appbar.AppBarLayout;\n\n/**\n * 与toolbar联动隐藏底部菜单\n *\n * @author LeonDevLifeLog <leondevlifelog@gmail.com>\n * @date 2018-02-24 08:59\n * @since V4.0.0\n */\npublic class BottomNavigationViewBehavior extends CoordinatorLayout.Behavior<View> {\n    public BottomNavigationViewBehavior() {\n        super();\n    }\n\n    public BottomNavigationViewBehavior(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    @Override\n    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {\n        ((CoordinatorLayout.LayoutParams) child.getLayoutParams()).topMargin = parent\n                .getMeasuredHeight() - child.getMeasuredHeight();\n        return super.onLayoutChild(parent, child, layoutDirection);\n    }\n\n    @Override\n    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {\n        return dependency instanceof AppBarLayout;\n    }\n\n    @Override\n    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {\n        //得到依赖View的滑动距离\n        int top = ((AppBarLayout.Behavior) ((CoordinatorLayout.LayoutParams) dependency\n                .getLayoutParams()).getBehavior()).getTopAndBottomOffset();\n        //因为BottomNavigation的滑动与ToolBar是反向的，所以取负值\n        ViewCompat.setTranslationY(child, -(top * child.getMeasuredHeight() / dependency\n                .getMeasuredHeight()));\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bridge/ArticleBridge.java",
    "content": "package me.wizos.loread.bridge;\n\n/**\n * 设置图片的默认加载行为\n * <p>\n * native 需要实现的接口有:\n * String readCacheFilePath(String url);\n * boolean isAutoLoadImage();\n * void loadImage(String url);\n * void openImage(String urls, int index);\n * <p>\n * native 可以调用的方法有:\n * void onImageLoadFailed(String url);\n * void onImageLoadSuccess(String url, String localUrl);\n *\n * @author by Wizos on 2018/3/4.\n */\n\npublic interface ArticleBridge {\n    String TAG = \"ArticleBridge\";\n\n    void log(String paramString);\n\n    /**\n     * 当图片快出现在屏幕上是，调用这个方法去加载图片\n     * 能否下载图片分以下几种情况：\n     * 1.网络不可用 → 返回失败占位图\n     * 2，开启省流量 & 蜂窝模式 → 返回点击下载占位图\n     * 3，开启省流量 & Wifi模式 → 返回正在下载占位图，开始下载\n     * 3，关闭省流量 & 蜂窝模式 → 返回正在下载占位图，开始下载\n     * 3，关闭省流量 & Wifi模式 → 返回正在下载占位图，开始下载\n     */\n    void readImage(String articleId,String imgHashCode, String originalUrl);\n    //void loadImage(String articleId,String imgHashCode, String url, String originalUrl);\n\n    //void downImage(String articleId,String imgHashCode, String originalUrl);\n\n    void downImage(String articleId,String imgHashCode, String originalUrl, boolean guessReferer);\n\n    void openImage(String articleId, String url);\n\n    void openLink(String link);\n\n    void openAudio(String link);\n\n    void readability();\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/bridge/WebBridge.java",
    "content": "package me.wizos.loread.bridge;\n\npublic interface WebBridge {\n    String TAG = \"WebBridge\";\n    void log(String msg);\n    void toggleScreenOrientation();\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/config/AdBlock.java",
    "content": "package me.wizos.loread.config;\n\nimport android.annotation.SuppressLint;\nimport android.net.Uri;\nimport android.os.AsyncTask;\n\nimport com.socks.library.KLog;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.util.HashSet;\nimport java.util.Locale;\nimport java.util.Set;\n\nimport me.wizos.loread.App;\n\n\npublic class AdBlock {\n    private transient static AdBlock instance;\n    private static final String FILE = \"ad_block.txt\";\n    private static final Set<String> hosts = new HashSet<>();\n    @SuppressLint(\"ConstantLocale\")\n    private static final Locale locale = Locale.getDefault();\n\n    private AdBlock() {}\n    public static AdBlock i() {\n        if (instance == null) {\n            synchronized (AdBlock.class) {\n                if (instance == null) {\n                    instance = new AdBlock();\n                    loadHosts();\n                }\n            }\n        }\n        return instance;\n    }\n    public void reset() {\n        instance = null;\n    }\n\n\n    private static void loadHosts() {\n        AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {\n            @Override\n            public void run() {\n                try {\n                    if( !new File(App.i().getGlobalAssetsFilesDir() + FILE).exists() ){\n                        return;\n                    }\n                    BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(App.i().getGlobalConfigPath() + \"ad_block.txt\")));\n                    String line;\n                    while ((line = reader.readLine()) != null) {\n                        hosts.add(line.toLowerCase(locale));\n                    }\n                } catch (IOException i) {\n                    i.printStackTrace();\n                    KLog.i(i);\n                }\n            }\n        });\n    }\n\n\n    private static String getDomain(String url) {\n        Uri uri = Uri.parse(url);\n        String domain = uri.getHost();\n\n        if (domain == null) {\n            url = url.toLowerCase(locale);\n            int index = url.indexOf('/', 8); // -> http://(7) and https://(8)\n            if (index != -1) {\n                url = url.substring(0, index);\n            }\n            return url;\n        }\n        return domain.startsWith(\"www.\") ? domain.substring(4) : domain;\n    }\n\n\n    public boolean isAd(String url) {\n        return hosts.contains(getDomain(url).toLowerCase(locale));\n    }\n\n\n//    public boolean isWhite(String url) {\n//        for (String domain : whitelist) {\n//            if (url.contains(domain)) {\n//                return true;\n//            }\n//        }\n//        return false;\n//    }\n\n//    private synchronized static void loadDomains(Context context) {\n//        RecordAction action = new RecordAction(context);\n//        action.open(false);\n//        whitelist.clear();\n//        for (String domain : action.listDomains()) {\n//            whitelist.add(domain);\n//        }\n//        action.close();\n//    }\n//    public synchronized void addDomain(String domain) {\n//        RecordAction action = new RecordAction(context);\n//        action.open(true);\n//        action.addDomain(domain);\n//        action.close();\n//        whitelist.add(domain);\n//    }\n//    public synchronized void removeDomain(String domain) {\n//        RecordAction action = new RecordAction(context);\n//        action.open(true);\n//        action.deleteDomain(domain);\n//        action.close();\n//        whitelist.remove(domain);\n//    }\n//    public synchronized void clearDomains() {\n//        RecordAction action = new RecordAction(context);\n//        action.open(true);\n//        action.clearDomains();\n//        action.close();\n//        whitelist.clear();\n//    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/config/ArticleActionConfig.java",
    "content": "package me.wizos.loread.config;\n\nimport android.text.TextUtils;\nimport android.util.ArrayMap;\n\nimport androidx.sqlite.db.SimpleSQLiteQuery;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.reflect.TypeToken;\nimport com.socks.library.KLog;\n\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.regex.Pattern;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.config.article_action_rule.ArticleActionRule;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.Entry;\nimport me.wizos.loread.network.callback.CallbackX;\nimport me.wizos.loread.utils.FileUtil;\nimport me.wizos.loread.utils.StringUtils;\n\npublic class ArticleActionConfig {\n    private static ArticleActionConfig instance;\n    private ArticleActionConfig() { }\n    public static ArticleActionConfig i() {\n        if (instance == null) {\n            synchronized (ArticleActionConfig.class) {\n                if (instance == null) {\n                    instance = new ArticleActionConfig();\n                    String json = FileUtil.readFile(App.i().getUserConfigPath() + \"article_action_rule.json\");\n                    if (TextUtils.isEmpty(json)) {\n                        instance.actionRuleArrayMap = new ArrayMap<String, ArticleActionRule>();\n                        instance.save();\n                    }else {\n                        instance.actionRuleArrayMap = new Gson().fromJson(json, new TypeToken<ArrayMap<String, ArticleActionRule>>() {}.getType());\n                    }\n                }\n            }\n        }\n        return instance;\n    }\n    public void save() {\n        FileUtil.save(App.i().getUserConfigPath() + \"article_action_rule.json\", new GsonBuilder().setPrettyPrinting().create().toJson(instance.actionRuleArrayMap));\n    }\n    public void reset() {\n        instance = null;\n    }\n\n\n    private ArrayMap<String, ArticleActionRule> actionRuleArrayMap;\n\n\n    public void exeRules(String uid, long timeMillis){\n        // 1.执行规则\n        for (Map.Entry<String, ArticleActionRule> entry: actionRuleArrayMap.entrySet()) {\n            exeRule(uid, entry.getValue(), timeMillis);\n        }\n    }\n    private void exeRule(String uid, ArticleActionRule articleActionRule, long timeMillis){\n        KLog.e(\"用户：\"+ uid + \" , \" + articleActionRule);\n        if(\"all\".equals(articleActionRule.getTarget())){\n            String sql = \"\";\n            if(\"contain\".equals(articleActionRule.getJudge())){\n                String[] keywords = articleActionRule.getValue().split(\"\\\\|\");\n                Set<String> conditions = new HashSet<>();\n                for (String keyword:keywords) {\n                    conditions.add( articleActionRule.getAttr() + \" like '%\" + keyword + \"%'\");\n                }\n                sql = StringUtils.join(\" or \", conditions);\n                SimpleSQLiteQuery query = new SimpleSQLiteQuery(\"SELECT * FROM article WHERE uid = ? AND crawlDate >= ? AND \" + sql, new Object[]{uid,timeMillis});\n                List<Article> articles = CoreDB.i().articleDao().getActionRuleArticlesRaw(query);\n                doActionWithArticles(articles, articleActionRule);\n                KLog.e(\"文章结果 为：\" + query.getSql() + \" == \" + articles.size());\n\n            }else if(\"not contain\".equals(articleActionRule.getJudge())){\n                String[] keywords = articleActionRule.getValue().split(\"\\\\|\");\n                Set<String> conditions = new HashSet<>();\n                for (String keyword:keywords) {\n                    conditions.add( articleActionRule.getAttr() + \" not like '%\" + keyword + \"%'\");\n                }\n                sql = StringUtils.join(\" and \", conditions);\n                SimpleSQLiteQuery query = new SimpleSQLiteQuery(\"SELECT * FROM article WHERE uid = ? AND crawlDate >= ? AND \" + sql, new Object[]{uid,timeMillis});\n                List<Article> articles = CoreDB.i().articleDao().getActionRuleArticlesRaw(query);\n                doActionWithArticles(articles, articleActionRule);\n                KLog.e(\"文章结果 为：\" + query.getSql() + \" == \" + articles.size());\n            }else if(\"match\".equals(articleActionRule.getJudge())){\n                List<String> needActionArticleIds = new ArrayList<>();\n                SimpleSQLiteQuery query = new SimpleSQLiteQuery(\"SELECT id, \" + articleActionRule.getAttr() + \" as entry FROM article WHERE uid = ? AND crawlDate >= ?\" + sql, new Object[]{uid,timeMillis});\n                List<Entry> entries = CoreDB.i().articleDao().getActionRuleArticlesRaw2(query);\n                Iterator<Entry> iterator = entries.iterator();\n                Pattern pattern = Pattern.compile(articleActionRule.getValue(), Pattern.CASE_INSENSITIVE);\n                Entry entry;\n                while (iterator.hasNext()){\n                    entry = iterator.next();\n                    if(pattern.matcher( entry.getEntry() ).find()){\n                        needActionArticleIds.add(entry.getId());\n                        iterator.remove();\n                    }\n                }\n                List<Article> articles = CoreDB.i().articleDao().getArticles(uid, needActionArticleIds);\n                doActionWithArticles(articles, articleActionRule);\n                KLog.e(\"文章结果 为：\" + query.getSql() + \" == \" + articles.size());\n            }else if(\"not match\".equals(articleActionRule.getJudge())){\n                List<String> needActionArticleIds = new ArrayList<>();\n                SimpleSQLiteQuery query = new SimpleSQLiteQuery(\"SELECT id, \" + articleActionRule.getAttr() + \" as entry FROM article WHERE uid = ? AND crawlDate >= ?\" + sql, new Object[]{uid,timeMillis});\n                List<Entry> entries = CoreDB.i().articleDao().getActionRuleArticlesRaw2(query);\n                Iterator<Entry> iterator = entries.iterator();\n                Pattern pattern = Pattern.compile(articleActionRule.getValue(), Pattern.CASE_INSENSITIVE);\n                Entry entry;\n                while (iterator.hasNext()){\n                    entry = iterator.next();\n                    if(!pattern.matcher( entry.getEntry() ).find()){\n                        needActionArticleIds.add(entry.getId());\n                        iterator.remove();\n                    }\n                }\n                List<Article> articles = CoreDB.i().articleDao().getArticles(uid, needActionArticleIds);\n                doActionWithArticles(articles, articleActionRule);\n                KLog.e(\"文章结果 为：\" + query.getSql() + \" == \" + articles.size());\n            }\n        }else if(articleActionRule.getTarget().startsWith(\"feed/\")){\n            String feedUrl = articleActionRule.getTarget().substring(5);\n            KLog.e(\"被处理的feedUrl为：\" + feedUrl);\n            String sql = \"\";\n            if(\"contain\".equals(articleActionRule.getJudge())){\n                String[] keywords = articleActionRule.getValue().split(\"\\\\|\");\n                Set<String> conditions = new HashSet<>();\n                for (String keyword:keywords) {\n                    conditions.add( \"article.\" + articleActionRule.getAttr() + \" like '%\" + keyword + \"%'\");\n                }\n                sql = StringUtils.join(\" or \", conditions);\n                SimpleSQLiteQuery query = new SimpleSQLiteQuery(\"SELECT article.* FROM article LEFT JOIN Feed ON (article.uid = Feed.uid AND article.feedId = Feed.id) WHERE article.uid = ? AND crawlDate >= ? AND Feed.feedUrl = ? AND \" + sql, new Object[]{uid, timeMillis, feedUrl});\n                List<Article> articles = CoreDB.i().articleDao().getActionRuleArticlesRaw(query);\n                doActionWithArticles(articles, articleActionRule);\n                KLog.e(\"文章结果 为：\" + query.getSql() + \" == \" + articles.size());\n            }else if(\"not contain\".equals(articleActionRule.getJudge())){\n                String[] keywords = articleActionRule.getValue().split(\"\\\\|\");\n                Set<String> conditions = new HashSet<>();\n                for (String keyword:keywords) {\n                    conditions.add( \"article.\" + articleActionRule.getAttr() + \" not like '%\" + keyword + \"%'\");\n                }\n                sql = StringUtils.join(\" and \", conditions);\n                SimpleSQLiteQuery query = new SimpleSQLiteQuery(\"SELECT article.* FROM article LEFT JOIN Feed ON (article.uid = Feed.uid AND article.feedId = Feed.id) WHERE article.uid = ? AND crawlDate >= ? AND Feed.feedUrl = ? AND \" + sql, new Object[]{uid,timeMillis, feedUrl});\n                List<Article> articles = CoreDB.i().articleDao().getActionRuleArticlesRaw(query);\n                doActionWithArticles(articles, articleActionRule);\n                KLog.e(\"文章结果 为：\" + query.getSql() + \" == \" + articles.size());\n            }else if(\"match\".equals(articleActionRule.getJudge())){\n                List<String> needActionArticleIds = new ArrayList<>();\n                SimpleSQLiteQuery query = new SimpleSQLiteQuery(\"SELECT article.id, article.\" + articleActionRule.getAttr() + \" as entry FROM article LEFT JOIN Feed ON (article.uid = Feed.uid AND article.feedId = Feed.id) WHERE article.uid = ? AND crawlDate >= ? AND Feed.feedUrl = ? \" + sql, new Object[]{App.i().getUser().getId(),timeMillis, feedUrl});\n                List<Entry> entries = CoreDB.i().articleDao().getActionRuleArticlesRaw2(query);\n                Iterator<Entry> iterator = entries.iterator();\n                Pattern pattern = Pattern.compile(articleActionRule.getValue(), Pattern.CASE_INSENSITIVE);\n                Entry entry;\n                while (iterator.hasNext()){\n                    entry = iterator.next();\n                    if(pattern.matcher( entry.getEntry() ).find()){\n                        needActionArticleIds.add(entry.getId());\n                        iterator.remove();\n                    }\n                }\n                List<Article> articles = CoreDB.i().articleDao().getArticles(uid, needActionArticleIds);\n                doActionWithArticles(articles, articleActionRule);\n                KLog.e(\"文章结果 为：\" + query.getSql() + \" == \" + articles.size());\n            }else if(\"not match\".equals(articleActionRule.getJudge())){\n                List<String> needActionArticleIds = new ArrayList<>();\n                SimpleSQLiteQuery query = new SimpleSQLiteQuery(\"SELECT article.id, article.\" + articleActionRule.getAttr() + \" as entry FROM article LEFT JOIN Feed ON (article.uid = Feed.uid AND article.feedId = Feed.id) WHERE article.uid = ? AND crawlDate >= ? AND Feed.feedUrl = ? \" + sql, new Object[]{uid,timeMillis, feedUrl});\n                List<Entry> entries = CoreDB.i().articleDao().getActionRuleArticlesRaw2(query);\n                Iterator<Entry> iterator = entries.iterator();\n                Pattern pattern = Pattern.compile(articleActionRule.getValue(), Pattern.CASE_INSENSITIVE);\n                Entry entry;\n                while (iterator.hasNext()){\n                    entry = iterator.next();\n                    if(!pattern.matcher( entry.getEntry() ).find()){\n                        needActionArticleIds.add(entry.getId());\n                        iterator.remove();\n                    }\n                }\n                List<Article> articles = CoreDB.i().articleDao().getArticles(uid, needActionArticleIds);\n                doActionWithArticles(articles, articleActionRule);\n                KLog.e(\"文章结果 为：\" + query.getSql() + \" == \" + articles.size());\n            }\n        }\n    }\n\n    private void doActionWithArticles(List<Article> articles, ArticleActionRule articleActionRule){\n        if(articleActionRule ==null || articleActionRule.getActions() == null || articleActionRule.getActions().size() == 0){\n            return;\n        }\n        if(articleActionRule.getActions().contains(\"mark read\")){\n            List<String> articleIds = new ArrayList<>();\n            for (Article article:articles) {\n                article.setReadStatus(App.STATUS_READED);\n                articleIds.add(article.getId());\n            }\n            App.i().getApi().markArticleListReaded(articleIds, new CallbackX() {\n                @Override\n                public void onSuccess(Object result) {\n                }\n\n                @Override\n                public void onFailure(Object error) {\n                }\n            });\n            CoreDB.i().articleDao().update(articles);\n        }\n\n        if(articleActionRule.getActions().contains(\"mark unreading\")){\n            for (Article article:articles) {\n                article.setReadStatus(App.STATUS_UNREADING);\n            }\n            CoreDB.i().articleDao().update(articles);\n        }\n\n        if(articleActionRule.getActions().contains(\"mark star\")){\n            for (Article article:articles) {\n                article.setReadStatus(App.STATUS_STARED);\n                App.i().getApi().markArticleStared(article.getId(), new CallbackX() {\n                    @Override\n                    public void onSuccess(Object result) {\n                    }\n\n                    @Override\n                    public void onFailure(Object error) {\n                    }\n                });\n            }\n            CoreDB.i().articleDao().update(articles);\n        }\n\n        if(articleActionRule.getActions().contains(\"delete\")){\n            CoreDB.i().articleDao().delete(articles);\n        }\n    }\n\n\n\n    private List<Article> getNeedActionArticlesWithNotMatch(List<Article> articles, Pattern pattern, String input){\n        Article article;\n        Iterator<Article> iterator = articles.iterator();\n        List<Article> needActionArticles = new ArrayList<>();\n        while (iterator.hasNext()){\n            article = iterator.next();\n            if(!pattern.matcher( input ).find()){\n                needActionArticles.add(article);\n                iterator.remove();\n            }\n        }\n        return needActionArticles;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/config/ArticleExtractConfig.java",
    "content": "package me.wizos.loread.config;\n\nimport android.text.TextUtils;\nimport android.util.ArrayMap;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.annotations.SerializedName;\nimport com.socks.library.KLog;\n\nimport org.jsoup.nodes.Document;\nimport org.jsoup.select.Elements;\n\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.config.article_extract_rule.ArticleExtractRule;\nimport me.wizos.loread.utils.FileUtil;\nimport me.wizos.loread.utils.StringUtils;\n\npublic class ArticleExtractConfig {\n    private transient static ArticleExtractConfig instance;\n    private ArticleExtractConfig() { }\n    public static ArticleExtractConfig i() {\n        if (instance == null) {\n            synchronized (ArticleExtractConfig.class) {\n                if (instance == null) {\n                    instance = new ArticleExtractConfig();\n                    String json = FileUtil.readFile(App.i().getUserConfigPath() + \"article_extract_rule.json\");\n                    if (TextUtils.isEmpty(json)) {\n                        instance.pageMatchRegex = new ArrayMap<String, ArticleExtractRule>();\n                        instance.pageMatchCssSelector = new ArrayMap<String, ArticleExtractRule>();\n                        instance.save();\n                    }else {\n                        instance = new Gson().fromJson(json, ArticleExtractConfig.class);\n                    }\n                }\n            }\n        }\n        return instance;\n    }\n    public void save() {\n        FileUtil.save(App.i().getUserConfigPath() + \"article_extract_rule.json\", new GsonBuilder().setPrettyPrinting().create().toJson(instance));\n    }\n    public void reset() {\n        instance = null;\n    }\n\n\n    @SerializedName(\"page_match_css_selector\")\n    private ArrayMap<String, ArticleExtractRule> pageMatchCssSelector;\n    @SerializedName(\"page_match_regex\")\n    private ArrayMap<String, ArticleExtractRule> pageMatchRegex;\n\n    public ArticleExtractRule getRuleByDomain(String domain){\n        String rules = FileUtil.readFile(  App.i().getUserConfigPath() + \"article_extract_rule/\" + domain + \".json\");\n        KLog.e(\"获取到的抓取规则内容：\"  + domain + \" ==  \" + rules);\n        if (!StringUtils.isEmpty(rules)) {\n            return new Gson().fromJson(rules, ArticleExtractRule.class);\n        }\n        return null;\n    }\n\n//    public void invalidRuleByDomain(String domain){\n//        File file = new File(App.i().getUserConfigPath() + \"article_extract_rule/\" + domain + \".json\");\n//        if(file.exists()){\n//            file.renameTo(new File(App.i().getUserConfigPath() + \"article_extract_rule_invalid/\" + domain + \".json\"));\n//        }\n//    }\n\n    public void saveRuleByDomain(Document document, String domain,  String oriCssSelector){\n        String optimizedCssSelector = optimizeCSSSelector(oriCssSelector);\n        if (document.select(optimizedCssSelector).size() == 1) {\n            saveSiteRule(domain, optimizedCssSelector);\n        } else {\n            saveSiteRule(domain, oriCssSelector);\n        }\n    }\n    private static final String RE_RULE1 = \" *(div|post|entry|article)(\\\\.[A-z0-9-_]+)*([.#])(entry|post|article)([-_])(content|article|body)([. ]|$)\";\n    private static final String RE_RULE2 = \" *(div|post|entry|article)(\\\\.[A-z0-9-_]+)*([.#])(entry|post|article|content|body)([. ]|$)\";\n    private static String optimizeCSSSelector(String cssQuery) {\n        Pattern pattern = Pattern.compile(RE_RULE1, Pattern.CASE_INSENSITIVE);\n        Matcher matcher = pattern.matcher(cssQuery);\n        if (matcher.find()) {\n            return matcher.group(1) + matcher.group(3) + matcher.group(4) + matcher.group(5) + matcher.group(6);\n        }\n        pattern = Pattern.compile(RE_RULE2, Pattern.CASE_INSENSITIVE);\n        matcher = pattern.matcher(cssQuery);\n        if (matcher.find()) {\n            return matcher.group(1) + matcher.group(3) + matcher.group(4);\n        }\n        return cssQuery;\n    }\n\n    private static void saveSiteRule(String domain, String cssSelector) {\n        ArticleExtractRule articleExtractRule = new ArticleExtractRule();\n        articleExtractRule.setContent(cssSelector);\n\n        Gson gson = new GsonBuilder()\n                .setPrettyPrinting() //对结果进行格式化，增加换行\n                .disableHtmlEscaping() //避免Gson使用时将一些字符自动转换为Unicode转义字符\n                .create();\n        FileUtil.save(App.i().getUserConfigPath() + \"article_extract_rule/\" + domain + \"_new.json\", gson.toJson(articleExtractRule, ArticleExtractRule.class));\n    }\n\n\n    public ArticleExtractRule getRuleByCssSelector(Document document){\n        if(pageMatchCssSelector == null || document == null){\n            return null;\n        }\n        Elements elements;\n        for (Map.Entry<String, ArticleExtractRule> entry:pageMatchCssSelector.entrySet()) {\n            elements = document.select(entry.getKey());\n            if(elements != null && elements.size() > 0) {\n                return entry.getValue();\n            }\n        }\n        return null;\n    }\n\n    public ArticleExtractRule getRuleByRegex(String page){\n        if(pageMatchRegex == null || StringUtils.isEmpty(page)){\n            return null;\n        }\n        Pattern pattern;\n        for (Map.Entry<String, ArticleExtractRule> entry:pageMatchRegex.entrySet()) {\n            pattern = Pattern.compile(entry.getKey(),Pattern.CASE_INSENSITIVE);\n            if(pattern.matcher(page).find()){\n                return entry.getValue();\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/config/ArticleTags.java",
    "content": "package me.wizos.loread.config;\n\nimport android.text.TextUtils;\nimport android.util.ArrayMap;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\n\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.db.ArticleTag;\nimport me.wizos.loread.utils.FileUtil;\n\n/**\n * 文章保存目录：\n *  1.默认目录（无分类）\n *  2.订阅源\n *  3.订阅源所属的分类（分类可能有多个，如何确定）\n *  4.保存时手动配置\n *\n *  自定义分类\n * @author Wizos on 2020/6/14.\n */\npublic class ArticleTags {\n    private static ArticleTags instance;\n    private ArticleTags() { }\n    public static ArticleTags i() {\n        if (instance == null) {\n            synchronized (ArticleTags.class) {\n                if (instance == null) {\n                    Gson gson = new Gson();\n                    String config = FileUtil.readFile(App.i().getUserFilesDir() + \"/config/article_tags.json\");\n                    if (TextUtils.isEmpty(config)) {\n                        instance = new ArticleTags();\n                        instance.articleTags = new ArrayMap<>();\n                        instance.tags = new HashSet<>();\n                    } else {\n                        instance = gson.fromJson(config, ArticleTags.class);\n                    }\n                }\n            }\n        }\n        return instance;\n    }\n    public void reset() {\n        instance = null;\n    }\n    public void save() {\n        FileUtil.save(App.i().getUserFilesDir() + \"/config/article_tags.json\", new GsonBuilder().setPrettyPrinting().create().toJson(instance));\n    }\n\n\n    private ArrayMap<String, Set<String>> articleTags;\n    private Set<String> tags;\n    public void removeArticle(String articleId){\n        articleTags.remove(articleId);\n    }\n    public void addArticleTags(List<ArticleTag> articleTags){\n        for (ArticleTag articleTag:articleTags) {\n            addArticleTag(articleTag);\n        }\n    }\n    public void addArticleTag(ArticleTag articleTag){\n        if(articleTag != null){\n            Set<String> tags;\n            if(articleTags.containsKey(articleTag.getArticleId())){\n                tags = articleTags.get(articleTag.getArticleId());\n            }else {\n                tags = new HashSet<>();\n            }\n            tags.add(articleTag.getTagId());\n            this.tags.add(articleTag.getTagId());\n            articleTags.put(articleTag.getArticleId(),tags);\n        }\n    }\n\n    public void newTag(String directory){\n        tags.add(directory);\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/config/HostConfig.java",
    "content": "//package me.wizos.loread.config;\n//\n//import android.net.Uri;\n//import android.text.TextUtils;\n//import android.util.ArrayMap;\n//\n//import com.google.gson.Gson;\n//import com.google.gson.GsonBuilder;\n//import com.google.gson.reflect.TypeToken;\n//\n//import me.wizos.loread.App;\n//import me.wizos.loread.utils.FileUtil;\n//\n///**\n// * @author Wizos on 2020/4/14.\n// */\n//public class HostConfig {\n//    private HostConfig() { }\n//    public static HostConfig i() {\n//        if (instance == null) {\n//            synchronized (HostConfig.class) {\n//                if (instance == null) {\n//                    Gson gson = new Gson();\n//                    instance = new HostConfig();\n//\n//                    String config = FileUtil.readFile(App.i().getGlobalConfigPath() + \"host_rewrite.json\");\n//                    if (TextUtils.isEmpty(config)) {\n//                        instance.domainRewrite = new ArrayMap<>();\n//                    } else {\n//                        instance.domainRewrite = gson.fromJson(config, new TypeToken<ArrayMap<String,String>>() {}.getType());\n//                    }\n//                }\n//            }\n//        }\n//        return instance;\n//    }\n//    public void reset() {\n//        instance = null;\n//    }\n//    public void save() {\n//        FileUtil.save(App.i().getGlobalConfigPath() + \"host_rewrite.json\", new GsonBuilder().setPrettyPrinting().create().toJson(instance.domainRewrite));\n//    }\n//\n//    private static HostConfig instance;\n//    private ArrayMap<String, String> domainRewrite;\n//\n//    //@SerializedName(\"url_match_domain_rewrite_url\")\n//\n//    public String getRedirectUrl(String url) {\n//        Uri uri = Uri.parse(url);\n//        String host = uri.getHost();\n//        if (domainRewrite.containsKey(host)) {\n//            assert host != null;\n//            return url.replaceFirst(host, domainRewrite.get(host));\n//        }\n//        return \"\";\n//    }\n//}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/config/LinkRewriteConfig.java",
    "content": "package me.wizos.loread.config;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport android.util.ArrayMap;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.annotations.SerializedName;\nimport com.socks.library.KLog;\n\nimport javax.script.Bindings;\nimport javax.script.SimpleBindings;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.utils.FileUtil;\nimport me.wizos.loread.utils.ScriptUtil;\n\n/**\n * @author Wizos on 2020/4/14.\n */\npublic class LinkRewriteConfig {\n    private LinkRewriteConfig() { }\n    public static LinkRewriteConfig i() {\n        if (instance == null) {\n            synchronized (LinkRewriteConfig.class) {\n                if (instance == null) {\n                    Gson gson = new Gson();\n\n                    String config = FileUtil.readFile(App.i().getUserConfigPath() + \"link_rewrite.json\");\n                    if (TextUtils.isEmpty(config)) {\n                        instance = new LinkRewriteConfig();\n                        instance.domainRewrite = new ArrayMap<>();\n                        instance.urlRewrite = new ArrayMap<>();\n                    } else {\n                        instance = gson.fromJson(config, LinkRewriteConfig.class);\n                    }\n                }\n            }\n        }\n        return instance;\n    }\n    public void reset() {\n        instance = null;\n    }\n    public void save() {\n        FileUtil.save(App.i().getUserConfigPath() + \"link_rewrite.json\", new GsonBuilder().setPrettyPrinting().create().toJson(instance));\n    }\n\n    @SerializedName(\"url_match_domain_rewrite_domain\")\n    private ArrayMap<String, String> domainRewrite;\n\n    @SerializedName(\"url_match_domain_rewrite_url\")\n    private ArrayMap<String, String> urlRewrite;\n    private static LinkRewriteConfig instance;\n\n    public String getRedirectUrl(String url) {\n        Uri uri = Uri.parse(url);\n        String host = uri.getHost();\n        if (domainRewrite.containsKey(host)) {\n            return url.replaceFirst(host, domainRewrite.get(host));\n        } else if (urlRewrite.containsKey(host)) {\n            // Bindings接口可以理解为上下文，可以往上下文中设置一个Java对象或通过key获取一个对象，它有一个实现类，SimpleBindings，内部就是一个map。\n            Bindings bindings = new SimpleBindings();\n            bindings.put(\"url\", url);\n            ScriptUtil.i().eval(urlRewrite.get(host), bindings);\n            KLog.i(\"重定向JS：\" + urlRewrite.get(host));\n            return (String) bindings.get(\"url\");\n        }\n        return \"\";\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/config/NetworkRefererConfig.java",
    "content": "package me.wizos.loread.config;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport android.util.ArrayMap;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.reflect.TypeToken;\n\nimport java.util.Arrays;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.utils.FileUtil;\nimport me.wizos.loread.utils.StringUtils;\n\npublic class NetworkRefererConfig {\n    private transient static NetworkRefererConfig instance;\n    public static NetworkRefererConfig i() {\n        if (instance == null) {\n            synchronized (NetworkRefererConfig.class) {\n                if (instance == null) {\n                    String json = FileUtil.readFile(App.i().getUserConfigPath() + \"network_referer.json\");\n                    instance = new NetworkRefererConfig();\n                    if (TextUtils.isEmpty(json)) {\n                        instance.domainReferer = new ArrayMap<String, String>();\n                    } else {\n                        instance.domainReferer = new Gson().fromJson(json, new TypeToken<ArrayMap<String,String>>() {}.getType());\n                    }\n                }\n            }\n        }\n        return instance;\n    }\n    public void save() {\n        FileUtil.save(App.i().getUserConfigPath() + \"network_referer.json\", new GsonBuilder().setPrettyPrinting().create().toJson(instance.domainReferer));\n    }\n    public void reset() {\n        instance = null;\n    }\n\n    private ArrayMap<String, String> domainReferer; // 格式是 domain, Referer\n\n    /**\n     * 用于手动下载图片\n     * 有3中方法获取referer：\n     * 1.根据feedid，推断出referer。。优点是简单，但是可能由于rss是第三方烧制的，可能会失效。\n     * 2.根据文章url，推断出referer。\n     * 2.根据图片url，猜测出referer，配置繁琐、低效，但是适应性较强。（可解决图片用的是第三方服务）\n     *\n     * @param imgUrl\n     * @return\n     */\n    public String guessRefererByUrl(String imgUrl) {\n        if (TextUtils.isEmpty(imgUrl)) {\n            return null;\n        }\n\n        Uri uri = Uri.parse(imgUrl);\n        String host = uri.getHost();\n        if (TextUtils.isEmpty(host)) {\n            return null;\n        }\n        if (domainReferer==null) {\n            return null;\n        }\n\n        if (domainReferer.containsKey(host)) {\n            return StringUtils.urlEncode(domainReferer.get(host));\n        }\n\n        String[] slices = host.split(\"\\\\.\");\n        for (int i = 1, size = slices.length; i+1 < size; i++) {\n            host = StringUtils.join(\".\", Arrays.copyOfRange(slices, i, size));\n//            KLog.i(\"分割 Host 推测 Referer：\" + host );\n            if (domainReferer.containsKey(host)) {\n                return domainReferer.get(host); // StringUtils.urlEncode();\n            }\n        }\n        return null;\n    }\n\n    public void addReferer(String imgUrl, String articleUrl){\n        Uri imgUri = Uri.parse(imgUrl);\n        String host = imgUri.getHost();\n        Uri articleUri = Uri.parse(articleUrl);\n        domainReferer.put(host, articleUri.getScheme() + \"://\" + articleUri.getHost());\n        save();\n    }\n\n\n    // https://blog.lyz810.com/article/2016/08/referrer-policy-and-anti-leech/\n    // https://www.jianshu.com/p/92bd520c0f8f\n    // https://www.jianshu.com/p/1be1f97167f8\n    public String getRefererByPolicy2(String refererPolicy, String articleUrl){\n        if(StringUtils.isEmpty(refererPolicy) || refererPolicy.equalsIgnoreCase(\"no-referrer\") || refererPolicy.equalsIgnoreCase(\"undefined\")){\n            return null;\n        }\n        if(refererPolicy.equalsIgnoreCase(\"no-referrer-when-downgrade\") || refererPolicy.equalsIgnoreCase(\"strict-origin\")){\n            if(!StringUtils.isEmpty(articleUrl) && articleUrl.startsWith(\"https://\")){\n                return articleUrl;\n            }\n            return null;\n        }\n\n        if(refererPolicy.equalsIgnoreCase(\"unsafe-url\")){\n            return articleUrl;\n        }\n\n        if(refererPolicy.equalsIgnoreCase(\"origin\")){\n            Uri uri = Uri.parse(articleUrl);\n            return uri.getScheme() + \"://\" + uri.getHost();\n        }\n        return null;\n    }\n\n    public String getRefererByPolicy(String refererPolicy, String articleUrl){\n        if(StringUtils.isEmpty(refererPolicy)){\n            return null;\n        }\n        if(refererPolicy.equalsIgnoreCase(\"no-referrer\") || refererPolicy.equalsIgnoreCase(\"undefined\")){\n            return null;\n        }\n        if(refererPolicy.equalsIgnoreCase(\"no-referrer-when-downgrade\") || refererPolicy.equalsIgnoreCase(\"strict-origin\") || refererPolicy.equalsIgnoreCase(\"origin\") || refererPolicy.equalsIgnoreCase(\"unsafe-url\")){\n            return articleUrl;\n        }\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/config/NetworkUserAgentConfig.java",
    "content": "package me.wizos.loread.config;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport android.util.ArrayMap;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.reflect.TypeToken;\n\nimport java.util.Arrays;\nimport java.util.Locale;\nimport java.util.Map;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.utils.FileUtil;\nimport me.wizos.loread.utils.StringUtils;\n\npublic class NetworkUserAgentConfig {\n    private static NetworkUserAgentConfig instance;\n    private NetworkUserAgentConfig() {}\n    public static NetworkUserAgentConfig i() {\n        if (instance == null) {\n            synchronized (NetworkUserAgentConfig.class) {\n                if (instance == null) {\n                    String config = FileUtil.readFile(App.i().getUserConfigPath() + \"network_user_agent.json\");\n                    instance = new NetworkUserAgentConfig();\n                    if (TextUtils.isEmpty(config)) {\n                        instance.domainUserAgent = new ArrayMap<String, String>();\n                        instance.userAgents = new ArrayMap<>();\n                        instance.userAgents.put(\"iPhone\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 11_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1\");\n                        instance.userAgents.put(\"Android\", \"Mozilla/5.0 (Linux; Android 5.1; MX5 Build/LMY47I) AppleWebKit/605.1.15 (KHTML, like Gecko) Chrome/66.0.3359.181 Mobile Safari/604.1\");\n                        instance.userAgents.put(\"Chrome(PC)\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36\");\n                    } else {\n                        instance.domainUserAgent = new Gson().fromJson(config, new TypeToken<ArrayMap<String,String>>() {}.getType());\n                    }\n                }\n            }\n        }\n        return instance;\n    }\n\n    public void save() {\n        FileUtil.save(App.i().getUserConfigPath() + \"network_user_agent.json\", new GsonBuilder().setPrettyPrinting().create().toJson(instance));\n    }\n\n    private ArrayMap<String, String> userAgents; // 格式是 Name, UA\n\n    private String holdUserAgent;\n    private int holdUserAgentIndex = -1;\n    private ArrayMap<String, String> domainUserAgent; // 格式是 Domain, Name\n\n\n    public String getHoldUserAgent() {\n        return holdUserAgent;\n    }\n\n    public void setHoldUserAgent(String holdUserAgent) {\n        this.holdUserAgent = holdUserAgent;\n    }\n\n    public ArrayMap<String, String> getUserAgents() {\n        return userAgents;\n    }\n\n\n    public String guessUserAgentByUrl(String url) {\n        if (!TextUtils.isEmpty(holdUserAgent)) {\n            return userAgents.get(holdUserAgent);\n        }\n        return guessUserAgentByUrl1(url);\n    }\n\n    /**\n     * 用于手动下载图片\n     * 有3中方法获取referer：\n     * 1.根据feedid，推断出referer。。优点是简单，但是可能由于rss是第三方烧制的，可能会失效。\n     * 2.根据文章url，推断出referer。\n     * 2.根据图片url，猜测出referer，配置繁琐、低效，但是适应性较强。（可解决图片用的是第三方服务）\n     *\n     * @param url\n     * @return\n     */\n    public String guessUserAgentByUrl1(String url) {\n        if (TextUtils.isEmpty(url)) {\n            return null;\n        }\n\n        Uri uri = Uri.parse(url);\n        String host = uri.getHost();\n        if (TextUtils.isEmpty(host)) {\n            return null;\n        }\n        if (domainUserAgent ==null) {\n            return null;\n        }\n\n        if (domainUserAgent.containsKey(host)) {\n            return StringUtils.urlEncode(domainUserAgent.get(host));\n        }\n\n        String[] slices = host.split(\"\\\\.\");\n        for (int i = 1, size = slices.length; i+1 < size; i++) {\n            host = StringUtils.join(\".\", Arrays.copyOfRange(slices, i, size));\n//            KLog.i(\"分割 Host 推测 UA：\" + host );\n            if (domainUserAgent.containsKey(host)) {\n                return domainUserAgent.get(host);\n            }\n        }\n        return null;\n    }\n\n    public String guessUserAgentByUrl2(String url) {\n        if (TextUtils.isEmpty(url)) {\n            return \"\";\n        }\n        url = url.toLowerCase(Locale.getDefault());\n        for (Map.Entry<String, String> entry : domainUserAgent.entrySet()) {\n            if (url.contains(entry.getKey())) {\n                return entry.getValue();\n            }\n        }\n        return \"\";\n    }\n\n    public void reset() {\n        instance = null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/config/SaveDirectory.java",
    "content": "package me.wizos.loread.config;\n\nimport android.text.TextUtils;\nimport android.util.ArrayMap;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.annotations.SerializedName;\n\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.db.Category;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.Feed;\nimport me.wizos.loread.utils.FileUtil;\nimport me.wizos.loread.utils.StringUtils;\n\n/**\n * 文章保存目录：\n *  1.默认目录（无分类）\n *  2.订阅源\n *  3.订阅源所属的分类（分类可能有多个，如何确定）\n *  4.保存时手动配置\n *\n *  自定义分类\n * @author Wizos on 2020/6/14.\n */\npublic class SaveDirectory {\n    // 按 feed 或者 category 级别来设置保存目录是只能设置为 root, feed, category\n    // 手动修改某个 文章 的保存目录时，可以设置为 root, feed, category, 目录名称\n    private String defaultDirectory; // 如果为null，代表根目录\n    @SerializedName(\"category\")\n    private ArrayMap<String, String> settingByCategory; // root, feed, category, 目录名称\n    @SerializedName(\"feed\")\n    private ArrayMap<String, String> settingByFeed; // root, feed, category, 目录名称\n    @SerializedName(\"article\")\n    private ArrayMap<String, String> settingByArticle;\n\n    private Set<String> directories;\n\n\n    private static SaveDirectory instance;\n\n    private SaveDirectory() { }\n\n    public static SaveDirectory i() {\n        if (instance == null) {\n            synchronized (SaveDirectory.class) {\n                if (instance == null) {\n                    Gson gson = new Gson();\n                    String config = FileUtil.readFile(App.i().getUserConfigPath() + \"/article_save_directory.json\");\n                    if (TextUtils.isEmpty(config)) {\n                        instance = new SaveDirectory();\n                        instance.settingByFeed = new ArrayMap<>();\n                        instance.settingByArticle = new ArrayMap<>();\n                        instance.directories = new HashSet<>();\n                    } else {\n                        instance = gson.fromJson(config, SaveDirectory.class);\n                    }\n                }\n            }\n        }\n        return instance;\n    }\n\n    public void reset() {\n        instance = null;\n    }\n    public void save() {\n        FileUtil.save(App.i().getUserConfigPath() + \"/article_save_directory.json\", new GsonBuilder().setPrettyPrinting().create().toJson(instance));\n    }\n\n\n    public String getDirNameSettingByFeed(String feedId){\n        String value;\n        if( settingByFeed != null && settingByFeed.containsKey(feedId) ){\n            value = settingByFeed.get(feedId);\n        }else {\n            value = defaultDirectory;\n        }\n        String name;\n        if(StringUtils.isEmpty(value) || \"loread_root\".equalsIgnoreCase(value)){\n            name = App.i().getString(R.string.root_directory);\n        }else if(\"loread_feed_title\".equalsIgnoreCase(value)){\n            name = App.i().getString(R.string.feed_title_as_directory);\n        }else if(\"loread_category_title\".equalsIgnoreCase(value)){\n            name = App.i().getString(R.string.category_title_as_directory);\n        }else {\n            name = value;\n        }\n        return name;\n    }\n//    public String getSavedDirectory(String feedId, String articleId) {\n//        if(settingByArticle != null && settingByArticle.containsKey(articleId)){\n//            return settingByArticle.get(articleId);\n//        }\n//\n//        if(settingByFeed != null && settingByFeed.containsKey(feedId)){\n//            String dir = settingByFeed.get(feedId);\n//            if(\"loread_feed\".equalsIgnoreCase(dir)){\n//                Feed feed = CoreDB.i().feedDao().getById(App.i().getUser().getId(), feedId);\n//                if(feed != null && !StringUtils.isEmpty(feed.getTitle())){\n//                    return feed.getTitle();\n//                }\n//            }\n////            else if(\"loread_category\".equalsIgnoreCase(dir)){\n////                List<Category> categories = CoreDB.i().categoryDao().getByFeedId(App.i().getUser().getId(), feedId);\n////                if(categories != null && !StringUtils.isEmpty(categories)){\n////                    List<String> titles = new ArrayList<>(categories.size());\n////                    for (Category category:categories) {\n////                        titles.add(category.getTitle());\n////                    }\n////                    return StringUtils.join(\"_\",titles);\n////                }\n////            }\n//            return settingByFeed.get(feedId);\n//        }\n//        return defaultDirectory;\n//    }\n\n//    String categoryId,\n    public String getSaveDir(String feedId, String articleId) {\n        // loread_root, loread_feed, loread_category, tag\n        String value;\n        if(settingByArticle != null && settingByArticle.containsKey(articleId)){\n            value = settingByArticle.get(articleId);\n        }else if(settingByFeed != null && settingByFeed.containsKey(feedId)){\n            value = settingByFeed.get(feedId);\n//        }else if( settingByCategory != null && settingByCategory.containsKey(categoryId) ){\n//            value = settingByCategory.get(categoryId);\n        }else {\n            value = defaultDirectory;\n        }\n\n        String dir = null;\n        if(\"loread_root\".equalsIgnoreCase(value)){\n        }else if(\"loread_feed_title\".equalsIgnoreCase(value)){\n            Feed feed = CoreDB.i().feedDao().getById(App.i().getUser().getId(), feedId);\n            if(feed != null){\n                dir = feed.getTitle();\n            }\n        }else if(\"loread_category_title\".equalsIgnoreCase(value)){\n            List<Category> categories = CoreDB.i().categoryDao().getByFeedId(App.i().getUser().getId(), feedId);\n            if(categories != null && !StringUtils.isEmpty(categories)){\n                List<String> titles = new ArrayList<>(categories.size());\n                for (Category category:categories) {\n                    titles.add(category.getTitle());\n                }\n                dir = StringUtils.join(\"_\",titles);\n            }\n        }else {\n            dir = value;\n        }\n        return dir;\n    }\n\n    public List<String> getDirectoriesOptionValue(){\n        List<String> dirs = new ArrayList<>();\n        dirs.add(\"loread_root\");\n        dirs.add(\"loread_feed_title\");\n        dirs.add(\"loread_category_title\");\n//        String[] dirs = new String[3];\n//        dirs[0] = \"loread_root\";\n//        dirs[1] = \"loread_feed_title\";\n//        dirs[2] = \"loread_category_title\";\n        return dirs;\n    }\n    public String[] getDirectoriesOptionName(){\n//        List<String> dirs = new ArrayList<>();\n//        dirs.add(App.i().getString(R.string.default_directory));\n//        dirs.add(App.i().getString(R.string.feed_title_as_directory));\n//        dirs.add(App.i().getString(R.string.category_title_as_directory));\n        //dirs.add(App.i().getString(R.string.custom_save_directory));\n\n        String[] dirs = new String[3];\n        dirs[0] = App.i().getString(R.string.root_directory);\n        dirs[1] = App.i().getString(R.string.feed_title_as_directory);\n        dirs[2] = App.i().getString(R.string.category_title_as_directory);\n        return dirs;\n    }\n\n    public void setFeedDirectory(String feedId, String directory){\n        if(StringUtils.isEmpty(directory)){\n            settingByFeed.remove(feedId);\n        }else {\n            settingByFeed.put(feedId,directory);\n        }\n    }\n\n    public void setArticleDirectory(String articleId, String directory){\n        if(StringUtils.isEmpty(directory)){\n            settingByArticle.remove(articleId);\n        }else {\n            settingByArticle.put(articleId,directory);\n        }\n    }\n\n    public void newDirectory(String directory){\n        directories.add(directory);\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/config/TestConfig.java",
    "content": "package me.wizos.loread.config;\n\nimport android.text.TextUtils;\nimport android.util.ArrayMap;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.reflect.TypeToken;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.utils.FileUtil;\n\n/**\n * @author Wizos on 2018/4/14.\n */\n\npublic class TestConfig {\n    private static TestConfig instance;\n    private TestConfig() { }\n\n    public static TestConfig i() {\n        if (instance == null) {\n            synchronized (TestConfig.class) {\n                if (instance == null) {\n                    instance = new TestConfig();\n                    Gson gson = new Gson();\n                    String config;\n                    config = FileUtil.readFile(App.i().getUserConfigPath() + \"config.json\");\n                    if (TextUtils.isEmpty(config)) {\n                        instance.displayRouter = new ArrayMap<String, String>();\n                        instance.save();\n                    } else {\n                        instance = gson.fromJson(config, new TypeToken<TestConfig>() {}.getType());\n                    }\n                }\n            }\n        }\n        return instance;\n    }\n\n    public void save() {\n        FileUtil.save(App.i().getUserConfigPath() + \"config.json\", new GsonBuilder().setPrettyPrinting().create().toJson(instance));\n    }\n    public void reset() {\n        instance = null;\n    }\n\n\n\n    private boolean ttsFile = false;\n    public int time = 60;\n\n    private ArrayMap<String, String> displayRouter;    // 格式是 feedId, mode\n\n\n    public boolean isTtsFile() {\n        return ttsFile;\n    }\n\n\n    public String getDisplayMode(String feedId) {\n        if (!TextUtils.isEmpty(feedId) && displayRouter != null && displayRouter.containsKey(feedId)) {\n            return displayRouter.get(feedId);\n        }\n        return App.DISPLAY_RSS;\n    }\n\n    public void addDisplayRouter(String feedId, String displayMode) {\n        displayRouter.put(feedId, displayMode);\n    }\n\n    public void removeDisplayRouter(String key) {\n        displayRouter.remove(key);\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/config/Unsubscribe.java",
    "content": "package me.wizos.loread.config;\n\n\nimport android.os.Environment;\n\nimport com.socks.library.KLog;\n\nimport org.jsoup.Jsoup;\nimport org.jsoup.nodes.Document;\nimport org.jsoup.nodes.Element;\nimport org.jsoup.nodes.Entities;\nimport org.jsoup.parser.Parser;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.bean.domain.OutFeed;\nimport me.wizos.loread.bean.domain.OutTag;\nimport me.wizos.loread.db.Category;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.Feed;\nimport me.wizos.loread.db.User;\nimport me.wizos.loread.utils.FileUtil;\n\n/**\n * Created by Wizos on 2019/5/14.\n */\n\npublic class Unsubscribe {\n    public static void genBackupFile2(User user, List<Feed> feeds) {\n        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {\n            KLog.e(\"外置存储设备不可用\");\n            return;\n        }\n        if (feeds.size() == 0) {\n            return;\n        }\n        File docDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);\n        if (!docDir.isDirectory()) {\n            return;\n        }\n\n        try {\n            String baseUri = \"loread://unsubscribe.feed\";\n            File file = new File(docDir.getAbsolutePath() + File.separator + user.getSource() + \"_\" + user.getUserName() + \"_unsubscribe.opml\");\n            Document doc;\n            Element bodyNode;\n            Element tagNode;\n            Element feedNode;\n\n            if (!file.exists()) {\n                Document.OutputSettings settings = new Document.OutputSettings();\n                settings.syntax(Document.OutputSettings.Syntax.xml).prettyPrint(true);\n                doc = new Document(baseUri).outputSettings(settings);\n                Element opml = doc.appendElement(\"opml\").attr(\"version\", \"1.0\");\n                doc.charset(Charset.defaultCharset());\n                String title = \"Subscriptions of \" + user.getUserName() + \" from \" + user.getSource();\n                opml.appendElement(\"head\").appendElement(\"title\").text(title);\n                bodyNode = opml.appendElement(\"body\");\n            } else {\n                doc = Jsoup.parse(FileUtil.readFile(file), baseUri, Parser.xmlParser());\n                bodyNode = doc.selectFirst(\"opml > body\");\n            }\n\n            List<Category> categories;\n            for (Feed feed : feeds) {\n                //categories = WithDB.i().getCategoriesByFeedId(feed.getId());\n                categories = CoreDB.i().categoryDao().getByFeedId(App.i().getUser().getId(),feed.getId());\n                for (Category category:categories){\n                    tagNode = doc.selectFirst(\"opml > body > outline[title=\\\"\" + category.getTitle() + \"\\\"]\");\n                    if (tagNode == null) {\n                        tagNode = bodyNode.appendElement(\"outline\").attr(\"title\", category.getTitle()).attr(\"text\", category.getTitle());\n                    }\n\n                    feedNode = tagNode.selectFirst(\"outline[title=\\\"\" + feed.getTitle() + \"\\\"]\");\n                    if (feedNode != null) {\n                        continue;\n                    }\n                    feedNode = createSelfClosingElement(\"outline\");\n                    feedNode.attr(\"title\", feed.getTitle())\n                            .attr(\"text\", feed.getTitle())\n                            .attr(\"type\", \"rss\")\n                            .attr(\"xmlUrl\", feed.getFeedUrl())\n                            .attr(\"htmlUrl\", feed.getHtmlUrl());\n                    tagNode.appendChild(feedNode);\n                }\n            }\n\n            FileUtil.save(file, doc.outerHtml());\n        } catch (IOException e) {\n            KLog.e(\"导出失败\");\n        }\n    }\n\n\n\n    @Deprecated\n    public static void genBackupFile(User user, String baseUri, ArrayList<OutTag> outTags) {\n        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {\n            KLog.e(\"外置存储设备不可用\");\n            return;\n        }\n        File docDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);\n        if (!docDir.isDirectory()) {\n            return;\n        }\n\n        try {\n            File file = new File(docDir.getAbsolutePath() + File.separator + user.getSource() + \"_\" + user.getUserName() + \"_unsubscribe.opml\");\n            Document doc;\n            if (file.exists()) {\n                // doc = Jsoup.parse(file, DataUtil.getCharsetFromContentType( FileUtil.readFile(file)),baseUri);\n                doc = Jsoup.parse(FileUtil.readFile(file), baseUri, Parser.xmlParser());\n                Element bodyNode = doc.selectFirst(\"opml > body\");\n                Element tagNode;\n                Element feedNode;\n                ArrayList<OutFeed> outFeeds;\n                for (OutTag outTag : outTags) {\n                    tagNode = doc.selectFirst(\"opml > body > outline[title=\\\"\" + outTag.getTitle() + \"\\\"]\");\n                    if (tagNode == null) {\n                        tagNode = bodyNode.appendElement(\"outline\").attr(\"title\", outTag.getTitle()).attr(\"text\", outTag.getTitle());\n                    }\n                    outFeeds = outTag.getOutFeeds();\n                    for (OutFeed outFeed : outFeeds) {\n                        feedNode = tagNode.selectFirst(\"outline[title=\\\"\" + outFeed.getTitle() + \"\\\"]\");\n                        if (feedNode != null) {\n                            continue;\n                        }\n                        feedNode = createSelfClosingElement(\"outline\");\n                        feedNode.attr(\"title\", outFeed.getTitle())\n                                .attr(\"text\", outFeed.getTitle())\n                                .attr(\"type\", \"rss\")\n                                .attr(\"xmlUrl\", outFeed.getFeedUrl())\n                                .attr(\"htmlUrl\", outFeed.getHtmlUrl());\n                        tagNode.appendChild(feedNode);\n                    }\n                }\n            } else {\n                Document.OutputSettings settings = new Document.OutputSettings();\n                settings.syntax(Document.OutputSettings.Syntax.xml).prettyPrint(true);\n                doc = new Document(baseUri).outputSettings(settings);\n                Element opml = doc.appendElement(\"opml\").attr(\"version\", \"1.0\");\n                doc.charset(Charset.defaultCharset());\n                String title = \"Subscriptions of \" + user.getUserName() + \" from \" + user.getSource();\n                opml.appendElement(\"head\").appendElement(\"title\").text(title);\n                Element body = opml.appendElement(\"body\");\n                Element tag;\n                Element feed;\n\n                ArrayList<OutFeed> outFeeds;\n                //KLog.e(\"获取A：\" + outTags.toString()  );\n                for (OutTag outTag : outTags) {\n                    tag = body.appendElement(\"outline\").attr(\"title\", outTag.getTitle()).attr(\"text\", outTag.getTitle());\n                    outFeeds = outTag.getOutFeeds();\n                    //KLog.e(\"获取B：\" + outFeeds.toString()  );\n                    for (OutFeed outFeed : outFeeds) {\n                        feed = createSelfClosingElement(\"outline\");\n                        feed.attr(\"title\", outFeed.getTitle())\n                                .attr(\"text\", outFeed.getTitle())\n                                .attr(\"type\", \"rss\")\n                                .attr(\"xmlUrl\", outFeed.getFeedUrl())\n                                .attr(\"htmlUrl\", outFeed.getHtmlUrl());\n                        tag.appendChild(feed);\n                    }\n                }\n            }\n            FileUtil.save(file, doc.outerHtml());\n        } catch (IOException e) {\n            KLog.e(\"导出失败\");\n        }\n    }\n\n    private static Element createSelfClosingElement(String tagName) {\n        return Jsoup.parseBodyFragment(\"<\" + tagName + \"/>\").body().child(0);\n    }\n\n    private static Element createVoidElement(String tagName, String baseUri) {\n        Document document = Jsoup.parse(\"<\" + tagName + \"/>\", baseUri, Parser.xmlParser());\n        document.outputSettings().prettyPrint(false);\n        document.outputSettings().escapeMode(Entities.EscapeMode.xhtml);\n        return document.body() == null ? document.child(0) : document.body().child(0);\n    }\n\n\n\n\n    public static void genBackupFile3(User user, List<Feed> feeds) {\n        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {\n            KLog.e(\"外置存储设备不可用\");\n            return;\n        }\n        File docDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);\n        if (!docDir.isDirectory()) {\n            return;\n        }\n\n        try {\n            String baseUri = \"loread://unsubscribe.feed\";\n            File file = new File(docDir.getAbsolutePath() + File.separator + user.getSource() + \"_\" + user.getUserName() + \"_unsubscribe.opml\");\n            Document doc;\n            Element bodyNode;\n            Element tagNode;\n            Element feedNode;\n\n            if (!file.exists()) {\n                Document.OutputSettings settings = new Document.OutputSettings();\n                settings.syntax(Document.OutputSettings.Syntax.xml).prettyPrint(true);\n                doc = new Document(baseUri).outputSettings(settings);\n                Element opml = doc.appendElement(\"opml\").attr(\"version\", \"1.0\");\n                doc.charset(Charset.defaultCharset());\n                String title = \"Subscriptions of \" + user.getUserName() + \" from \" + user.getSource();\n                opml.appendElement(\"head\").appendElement(\"title\").text(title);\n                bodyNode = opml.appendElement(\"body\");\n            } else {\n                doc = Jsoup.parse(FileUtil.readFile(file), baseUri, Parser.xmlParser());\n                bodyNode = doc.selectFirst(\"opml > body\");\n            }\n\n            List<Category> categories;\n            for (Feed feed : feeds) {\n                //categories = WithDB.i().getCategoriesByFeedId(feed.getId());\n                categories = CoreDB.i().categoryDao().getByFeedId(App.i().getUser().getId(),feed.getId());\n                for (Category category:categories){\n                    tagNode = doc.selectFirst(\"opml > body > outline[title=\\\"\" + category.getTitle() + \"\\\"]\");\n                    if (tagNode == null) {\n                        tagNode = bodyNode.appendElement(\"outline\").attr(\"title\", category.getTitle()).attr(\"text\", category.getTitle());\n                    }\n\n                    feedNode = tagNode.selectFirst(\"outline[title=\\\"\" + feed.getTitle() + \"\\\"]\");\n                    if (feedNode != null) {\n                        continue;\n                    }\n                    feedNode = createSelfClosingElement(\"outline\");\n                    feedNode.attr(\"title\", feed.getTitle())\n                            .attr(\"text\", feed.getTitle())\n                            .attr(\"type\", \"rss\")\n                            .attr(\"xmlUrl\", feed.getFeedUrl())\n                            .attr(\"htmlUrl\", feed.getHtmlUrl());\n                    tagNode.appendChild(feedNode);\n                }\n            }\n\n            FileUtil.save(file, doc.outerHtml());\n        } catch (IOException e) {\n            KLog.e(\"导出失败\");\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/config/article_action_rule/ArticleActionRule.java",
    "content": "package me.wizos.loread.config.article_action_rule;\n\nimport java.util.Set;\n\npublic class ArticleActionRule {\n    private String target; // all, feed;    // category:\n    private String attr; // title, content, author, link\n    //private String type; // keyword, regex;     js\n    private String judge; // 匹配正则，未匹配正则；包含关键词，未包含关键词\n    private String value;\n    private Set<String> actions;\n\n    public String getTarget() {\n        return target;\n    }\n\n    public void setTarget(String target) {\n        this.target = target;\n    }\n\n    public String getAttr() {\n        return attr;\n    }\n\n    public void setAttr(String attr) {\n        this.attr = attr;\n    }\n\n    public String getJudge() {\n        return judge;\n    }\n\n    public void setJudge(String judge) {\n        this.judge = judge;\n    }\n\n    public String getValue() {\n        return value;\n    }\n\n    public void setValue(String value) {\n        this.value = value;\n    }\n\n    public Set<String> getActions() {\n        return actions;\n    }\n\n    public void setActions(Set<String> actions) {\n        this.actions = actions;\n    }\n\n    @Override\n    public String toString() {\n        return \"ActionRule{\" +\n                \"target='\" + target + '\\'' +\n                \", attr='\" + attr + '\\'' +\n                \", judge='\" + judge + '\\'' +\n                \", value='\" + value + '\\'' +\n                \", actions=\" + actions +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/config/article_action_rule/Condition.java",
    "content": "//package me.wizos.loread.config.article_action_rule;\n//\n//public class Condition {\n//    private String target;\n//    private String attr;\n//    //private String type; // keyword, regex;     js\n//    private String judge; // 匹配正则，未匹配正则；包含关键词，未包含关键词\n//    private String value;\n//\n//    public String getTarget() {\n//        return target;\n//    }\n//\n//    public void setTarget(String target) {\n//        this.target = target;\n//    }\n//\n//    public String getAttr() {\n//        return attr;\n//    }\n//\n//    public void setAttr(String attr) {\n//        this.attr = attr;\n//    }\n//\n//    public String getJudge() {\n//        return judge;\n//    }\n//\n//    public void setJudge(String judge) {\n//        this.judge = judge;\n//    }\n//\n//    public String getValue() {\n//        return value;\n//    }\n//\n//    public void setValue(String value) {\n//        this.value = value;\n//    }\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/config/article_extract_rule/ArticleExtractRule.java",
    "content": "package me.wizos.loread.config.article_extract_rule;\n\nimport com.google.gson.annotations.SerializedName;\n\npublic class ArticleExtractRule {\n    private Selector selector = Selector.css;\n\n    @SerializedName(\"document_trim\")\n    private String documentTrim;\n\n    private String content;\n\n    @SerializedName(\"content_strip\")\n    private String contentStrip;\n\n    @SerializedName(\"content_trim\")\n    private String contentTrim;\n\n    public Selector getSelector() {\n        return selector;\n    }\n\n    public String getDocumentTrim() {\n        return documentTrim;\n    }\n\n    public String getContent() {\n        return content;\n    }\n\n    public void setContent(String content) {\n        this.content = content;\n    }\n\n    public String getContentStrip() {\n        return contentStrip;\n    }\n\n    public String getContentTrim() {\n        return contentTrim;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/config/article_extract_rule/Selector.java",
    "content": "package me.wizos.loread.config.article_extract_rule;\n\npublic enum Selector {\n    css(\"css\"),xpath(\"xpath\"),regex(\"regex\");\n\n    private String selector;\n\n    Selector(String selector) { }\n\n    public String getSelector() {\n        return selector;\n    }\n\n    public void setSelector(String selector) {\n        this.selector = selector;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/Article.java",
    "content": "package me.wizos.loread.db;\n\n\nimport androidx.annotation.NonNull;\nimport androidx.room.Entity;\nimport androidx.room.ForeignKey;\nimport androidx.room.Index;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport me.wizos.loread.App;\n\n/**\n * //    private Integer preference = 0; // 偏好（点击）：0是初始状态，1是不喜欢，2是喜欢\n * //    private Integer predict = 0; //推断结果： 0是初始状态，1是不喜欢，2是喜欢\n * Created by Wizos on 2020/3/17.\n */\n@Entity(primaryKeys = {\"id\",\"uid\"},\n        indices = { @Index({\"id\"}),@Index({\"uid\"}),@Index({\"title\"}),@Index({\"feedId\",\"uid\"}),@Index({\"readStatus\"}),@Index({\"starStatus\"}),@Index({\"saveStatus\"})},\n        foreignKeys = @ForeignKey(entity = User.class, parentColumns = \"id\", childColumns = \"uid\")\n        )\npublic class Article implements Cloneable{\n    @NonNull\n    private String id;\n    @NonNull\n    private String uid;\n    private String title;\n    private String content;\n    private String summary;\n    private String image;\n    private String enclosure; // 包含图片，视频等多媒体信息\n\n    private String feedId;\n    private String feedTitle;\n    private String author;\n    private String link = \"\";\n    private long pubDate;\n    private long crawlDate;\n    private int readStatus = App.STATUS_UNREAD;\n    private int starStatus = App.STATUS_UNSTAR;\n    private int saveStatus = App.STATUS_NOT_FILED;\n    private long readUpdated;\n    private long starUpdated;\n\n\n    @NotNull\n    public String getUid() {\n        return uid;\n    }\n\n    public void setUid(String uid) {\n        this.uid = uid;\n    }\n\n    @NotNull\n    public String getId() {\n        return this.id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getTitle() {\n        return this.title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    public String getContent() {\n        return this.content;\n    }\n\n    public void setContent(String content) {\n        this.content = content;\n    }\n\n    public String getSummary() {\n        return this.summary;\n    }\n\n    public void setSummary(String summary) {\n        this.summary = summary;\n    }\n\n    public String getImage() {\n        return this.image;\n    }\n\n    public void setImage(String image) {\n        this.image = image;\n    }\n\n    public String getEnclosure() {\n        return this.enclosure;\n    }\n\n    public void setEnclosure(String enclosure) {\n        this.enclosure = enclosure;\n    }\n\n    public String getFeedId() {\n        return this.feedId;\n    }\n\n    public void setFeedId(String feedId) {\n        this.feedId = feedId;\n    }\n\n    public String getFeedTitle() {\n        return this.feedTitle;\n    }\n\n    public void setFeedTitle(String feedTitle) {\n        this.feedTitle = feedTitle;\n    }\n\n    public String getAuthor() {\n        return this.author;\n    }\n\n    public void setAuthor(String author) {\n        this.author = author;\n    }\n\n    public String getLink() {\n        return this.link;\n    }\n\n    public void setLink(String link) {\n        this.link = link;\n    }\n\n    public long getPubDate() {\n        return this.pubDate;\n    }\n\n    public void setPubDate(long pubDate) {\n        this.pubDate = pubDate;\n    }\n\n    public long getCrawlDate() {\n        return this.crawlDate;\n    }\n\n    public void setCrawlDate(long crawlDate) {\n        this.crawlDate = crawlDate;\n    }\n\n    public int getReadStatus() {\n        return this.readStatus;\n    }\n\n    public void setReadStatus(int readStatus) {\n        this.readStatus = readStatus;\n    }\n\n    public int getStarStatus() {\n        return this.starStatus;\n    }\n\n    public void setStarStatus(int starStatus) {\n        this.starStatus = starStatus;\n    }\n\n    public int getSaveStatus() {\n        return this.saveStatus;\n    }\n\n    public void setSaveStatus(int saveStatus) {\n        this.saveStatus = saveStatus;\n    }\n\n    public long getReadUpdated() {\n        return readUpdated;\n    }\n\n    public void setReadUpdated(long readUpdated) {\n        this.readUpdated = readUpdated;\n    }\n\n    public long getStarUpdated() {\n        return starUpdated;\n    }\n\n    public void setStarUpdated(long starUpdated) {\n        this.starUpdated = starUpdated;\n    }\n\n    @Override\n    public Object clone(){\n        try{\n            return super.clone();   //浅复制\n        }catch(CloneNotSupportedException e) {\n            e.printStackTrace();\n        }\n        return this;\n    }\n    @Override\n    public String toString() {\n        return \"Article{\" +\n                \"id='\" + id + '\\'' +\n                \", uid='\" + uid + '\\'' +\n                \", title='\" + title + '\\'' +\n                \", summary='\" + summary + '\\'' +\n                \", image='\" + image + '\\'' +\n                \", enclosure='\" + enclosure + '\\'' +\n                \", feedId='\" + feedId + '\\'' +\n                \", feedTitle='\" + feedTitle + '\\'' +\n                \", author='\" + author + '\\'' +\n                \", link='\" + link + '\\'' +\n                \", pubDate=\" + pubDate +\n                \", crawlDate=\" + crawlDate +\n                \", readStatus=\" + readStatus +\n                \", starStatus=\" + starStatus +\n                \", saveStatus=\" + saveStatus +\n                \", readUpdated=\" + readUpdated +\n                \", starUpdated=\" + starUpdated +\n                \", content='\" + content +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/ArticleDao.java",
    "content": "package me.wizos.loread.db;\n\nimport androidx.paging.DataSource;\nimport androidx.room.Dao;\nimport androidx.room.Delete;\nimport androidx.room.Insert;\nimport androidx.room.OnConflictStrategy;\nimport androidx.room.Query;\nimport androidx.room.RawQuery;\nimport androidx.room.Transaction;\nimport androidx.room.Update;\nimport androidx.sqlite.db.SupportSQLiteQuery;\n\nimport java.util.List;\n\nimport me.wizos.loread.App;\n\n/**\n * DataSource的三个子类：\n * PositionalDataSource: 主要用于加载数据可数有限的数据。比如加载本地数据库，这种情况下用户可以通过比如说像通讯录按姓的首字母查询的情况。能够跳转到任意的位置。\n * ItemKeyedDataSource:主要用于加载逐渐增加的数据。比如说网络请求的数据随着不断的请求得到的数据越来越多。然后它适用的情况就是通过N-1item的数据来获取Nitem数据的情况。比如说Github的api。\n * PageKeyedDataSource:这个和ItemKeyedDataSource有些相似，都是针对那种不断增加的数据。这里网络请求得到数据是分页的。比如说知乎日报的news的api。\n *\n * 从 Read 属性的4个值(Readed, UnRead, UnReading, All), Star 属性的3个类型(Stared, UnStar, All)中，抽出 UnRead(含UnReading), Stared, All 3个快捷状态，供用户在主页面切换时使用\n * 根据 StreamId 来获取文章，可从2个属性( Categories[针对Tag], OriginStreamId[针对Feed] )上，共4个变化上（All, TTRSSCategoryItem, NoTag, TTRSSFeedItem）来获取文章。\n * 据 StreamState 也是从2个属性(ReadState, StarState)的3个快捷状态 ( UnRead[含UnReading], Stared, All ) 来获取文章。\n * 所以文章列表页会有6种组合：某个 Categories 内的 UnRead[含UnReading], Stared, All。某个 OriginStreamId 内的 UnRead[含UnReading], Stared, All。\n */\n@Dao\npublic interface ArticleDao {\n    @Query(\"SELECT * FROM article WHERE uid = :uid AND id = :id LIMIT 1\")\n    Article getById(String uid, String id);\n\n//    @Query(\"SELECT * FROM article WHERE uid = :uid \" +\n//            \"ORDER BY crawlDate DESC,pubDate DESC\")\n//    Cursor getAll2(String uid);\n//    @Query(\"SELECT * FROM article \" +\n//            \"WHERE uid = :uid \" +\n//            \"AND (article.readStatus = \" + App.STATUS_UNREAD  + \" OR article.readStatus = \" + App.STATUS_UNREADING  + \" OR (article.readStatus = \" + App.STATUS_READED +\" AND article.readUpdated > :timeMillis) ) \" +\n//            \"ORDER BY crawlDate DESC,pubDate DESC\")\n//    DataSource.Factory<Integer,Article> getUnread2(String uid,long timeMillis);\n\n    @Query(\"SELECT * FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"AND crawlDate < :timeMillis \" +\n            \"ORDER BY crawlDate DESC,pubDate DESC\")\n    DataSource.Factory<Integer,Article> getAll(String uid,long timeMillis);\n\n    @Query(\"SELECT * FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"AND crawlDate < :timeMillis \" +\n            \"AND (article.starStatus = \" + App.STATUS_STARED + \"  OR (article.starStatus = \" + App.STATUS_UNSTAR +\" AND article.starUpdated > :timeMillis))\" +\n            \"ORDER BY crawlDate DESC,pubDate DESC\")\n    DataSource.Factory<Integer,Article> getStared(String uid,long timeMillis);\n\n    @Query(\"SELECT * FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"AND crawlDate < :timeMillis \" +\n            \"AND (article.readStatus = \" + App.STATUS_UNREAD  + \" OR article.readStatus = \" + App.STATUS_UNREADING + \" OR (article.readStatus = \" + App.STATUS_READED +\" AND article.readUpdated > :timeMillis)) \" +\n            \"ORDER BY crawlDate DESC,pubDate DESC\")\n    DataSource.Factory<Integer,Article> getUnread(String uid,long timeMillis);\n\n    @Query(\"SELECT article.* FROM article \" +\n            \"LEFT JOIN FeedCategory ON (article.uid = FeedCategory.uid AND article.feedId = FeedCategory.feedId)\" +\n            \"WHERE article.uid = :uid \" +\n            \"AND article.crawlDate < :timeMillis \" +\n            \"AND FeedCategory.categoryId = :categoryId \" +\n            \"ORDER BY crawlDate DESC,pubDate DESC\")\n    DataSource.Factory<Integer,Article> getAllByCategoryId(String uid, String categoryId, long timeMillis);\n    @Query(\"SELECT article.* FROM article \" +\n            \"LEFT JOIN FeedCategory ON (article.uid = FeedCategory.uid AND article.feedId = FeedCategory.feedId)\" +\n            \"WHERE article.uid = :uid \" +\n            \"AND crawlDate < :timeMillis \" +\n            \"AND FeedCategory.categoryId = :categoryId \" +\n            \"AND (article.readStatus = \" + App.STATUS_UNREAD  + \" OR article.readStatus = \" + App.STATUS_UNREADING + \" OR (article.readStatus = \" + App.STATUS_READED +\" AND article.readUpdated > :timeMillis) ) \" +\n            \"ORDER BY crawlDate DESC,pubDate DESC\")\n    DataSource.Factory<Integer,Article> getUnreadByCategoryId(String uid, String categoryId,long timeMillis);\n    @Query(\"SELECT article.* FROM article \" +\n            \"LEFT JOIN FeedCategory ON (article.uid = FeedCategory.uid AND article.feedId = FeedCategory.feedId)\" +\n            \"WHERE article.uid = :uid \" +\n            \"AND article.crawlDate < :timeMillis \" +\n            \"AND FeedCategory.categoryId = :categoryId \" +\n            \"AND (article.starStatus = \" + App.STATUS_STARED  + \" OR (article.starStatus = \" + App.STATUS_UNSTAR +\" AND article.starUpdated > :timeMillis) )\" +\n            \"ORDER BY crawlDate DESC,pubDate DESC\")\n    DataSource.Factory<Integer,Article> getStaredByCategoryId(String uid, String categoryId, long timeMillis);\n\n\n    @Query(\"SELECT article.* FROM article \" +\n            \"LEFT JOIN ArticleTag ON (article.uid = ArticleTag.uid AND article.id = ArticleTag.articleId)\" +\n            \"WHERE article.uid = :uid \" +\n            \"AND article.crawlDate < :timeMillis \" +\n            \"AND ArticleTag.tagId = :tagId \" +\n            \"AND (article.starStatus = \" + App.STATUS_STARED  + \" OR (article.starStatus = \" + App.STATUS_UNSTAR +\" AND article.starUpdated > :timeMillis) )\" +\n            \"ORDER BY crawlDate DESC,pubDate DESC\")\n    DataSource.Factory<Integer,Article> getStaredByTagId(String uid, String tagId, long timeMillis);\n\n    @Query(\"SELECT article.* FROM article \" +\n            \"LEFT JOIN FeedCategory ON (article.uid = FeedCategory.uid AND article.feedId = FeedCategory.feedId)\" +\n            \"WHERE article.uid = :uid \" +\n            \"AND article.crawlDate < :timeMillis \" +\n            \"AND FeedCategory.categoryId is NULL \" +\n            \"ORDER BY crawlDate,pubDate DESC\")\n    DataSource.Factory<Integer,Article> getAllByUncategory(String uid, long timeMillis);\n    @Query(\"SELECT article.* FROM article \" +\n            \"LEFT JOIN FeedCategory ON (article.uid = FeedCategory.uid AND article.feedId = FeedCategory.feedId)\" +\n            \"WHERE article.uid = :uid \" +\n            \"AND article.crawlDate < :timeMillis \" +\n            \"AND FeedCategory.categoryId is NULL \" +\n            \"AND (article.readStatus = \" + App.STATUS_UNREAD  + \" OR article.readStatus = \" + App.STATUS_UNREADING + \" OR (article.readStatus = \" + App.STATUS_READED +\" AND article.readUpdated > :timeMillis) ) \" +\n            \"ORDER BY crawlDate,pubDate DESC\")\n    DataSource.Factory<Integer,Article> getUnreadByUncategory(String uid, long timeMillis);\n    @Query(\"SELECT article.* FROM article \" +\n            \"LEFT JOIN FeedCategory ON (article.uid = FeedCategory.uid AND article.feedId = FeedCategory.feedId)\" +\n            \"WHERE article.uid = :uid \" +\n            \"AND article.crawlDate < :timeMillis \" +\n            \"AND FeedCategory.categoryId is NULL \" +\n            \"AND (article.starStatus = \" + App.STATUS_STARED + \" OR (article.starStatus = \" + App.STATUS_UNSTAR +\" AND article.starUpdated > :timeMillis) ) \" +\n            \"ORDER BY crawlDate DESC,pubDate DESC\")\n    DataSource.Factory<Integer,Article> getStaredByUncategory(String uid, long timeMillis);\n\n    @Query(\"SELECT article.* FROM article \" +\n            \"LEFT JOIN ArticleTag ON (article.uid = articletag.uid AND article.id = articletag.articleId)\" +\n            \"WHERE article.uid = :uid \" +\n            \"AND article.crawlDate < :timeMillis \" +\n            \"AND articletag.tagId is NULL \" +\n            \"AND (article.starStatus = \" + App.STATUS_STARED + \" OR (article.starStatus = \" + App.STATUS_UNSTAR +\" AND article.starUpdated > :timeMillis) ) \" +\n            \"ORDER BY crawlDate DESC,pubDate DESC\")\n    DataSource.Factory<Integer,Article> getStaredByUnTag(String uid, long timeMillis);\n\n\n    @Query(\"SELECT * FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"AND article.crawlDate < :timeMillis \" +\n            \"AND feedId = :feedId \" +\n            \"ORDER BY crawlDate DESC,pubDate DESC\")\n    DataSource.Factory<Integer,Article> getAllByFeedId(String uid, String feedId, long timeMillis);\n    @Query(\"SELECT * FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"AND article.crawlDate < :timeMillis \" +\n            \"AND feedId = :feedId \" +\n            \"AND (readStatus = \" + App.STATUS_UNREAD  + \" OR readStatus = \" + App.STATUS_UNREADING + \" OR (article.readStatus = \" + App.STATUS_READED +\" AND article.readUpdated > :timeMillis) ) \" +\n            \"ORDER BY crawlDate DESC,pubDate DESC\")\n    DataSource.Factory<Integer,Article> getUnreadByFeedId(String uid, String feedId, long timeMillis);\n    @Query(\"SELECT * FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"AND article.crawlDate < :timeMillis \" +\n            \"AND feedId = :feedId \" +\n            \"AND (starStatus = \" + App.STATUS_STARED + \" OR (article.starStatus = \" + App.STATUS_UNSTAR +\" AND article.starUpdated > :timeMillis) ) \" +\n            \"ORDER BY crawlDate DESC,pubDate DESC\")\n    DataSource.Factory<Integer,Article> getStaredByFeedId(String uid, String feedId, long timeMillis);\n\n    @Query(\"SELECT * FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"AND title LIKE :keyword \" +\n            \"ORDER BY crawlDate DESC,pubDate DESC\")\n    DataSource.Factory<Integer,Article> getAllByKeyword(String uid, String keyword);\n\n//    @Query(\"DELETE FROM article WHERE uid = :uid AND pubDate < :timeMillis\")\n//    void clearPubDate(String uid,long timeMillis);\n\n//    @Query(\"SELECT * FROM article \" +\n//            \"WHERE uid = :uid \" +\n//            \"AND (article.readStatus = \" + App.STATUS_UNREAD  + \" OR article.readStatus = \" + App.STATUS_UNREADING  + \" OR article.starStatus = \" + App.STATUS_STARED  + \") \" +\n//            \"ORDER BY crawlDate DESC,pubDate DESC\")\n//    List<Article> getValuable(String uid);\n\n//    @Query(\"SELECT article.* FROM article \" +\n//            \"LEFT JOIN FeedCategory ON (article.uid = FeedCategory.uid AND article.feedId = FeedCategory.feedId)\" +\n//            \"WHERE article.uid = :uid \" +\n//            \"AND FeedCategory.categoryId is NULL \" +\n//            \"AND (article.readStatus = \" + App.STATUS_UNREAD  + \" OR article.readStatus = \" + App.STATUS_UNREADING  + \" OR article.starStatus = \" + App.STATUS_STARED  + \") \" +\n//            \"ORDER BY crawlDate,pubDate DESC\")\n//    Cursor getValuableByUnCategory(String uid);\n\n//    @Query(\"SELECT article.* FROM article \" +\n//            \"LEFT JOIN FeedCategory ON (article.uid = FeedCategory.uid AND article.feedId = FeedCategory.feedId)\" +\n//            \"WHERE article.uid = :uid \" +\n//            \"AND FeedCategory.categoryId = :categoryId \" +\n//            \"AND (article.readStatus = \" + App.STATUS_UNREAD  + \" OR article.readStatus = \" + App.STATUS_UNREADING  + \" OR article.starStatus = \" + App.STATUS_STARED  + \") \" +\n//            \"ORDER BY crawlDate DESC,pubDate DESC\")\n//    Cursor getValuableByCategoryId(String uid, String categoryId);\n\n\n    @Query(\"SELECT * FROM article WHERE uid = :uid\")\n    List<Article> getAllNoOrder(String uid);\n\n    @Query(\"SELECT * FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"AND crawlDate > :timeMillis \")\n    List<Article> getAllNoOrder(String uid,long timeMillis);\n\n\n    @Query(\"SELECT * FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"AND article.starStatus = \" + App.STATUS_STARED)\n    List<Article> getStaredNoOrder(String uid);\n    @Query(\"SELECT * FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"AND (article.readStatus = \" + App.STATUS_UNREAD  + \" OR article.readStatus = \" + App.STATUS_UNREADING  + \") \")\n    List<Article>  getUnreadNoOrder(String uid);\n\n    @Query(\"SELECT count(1) FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"AND (article.readStatus = \" + App.STATUS_UNREAD  + \" OR article.readStatus = \" + App.STATUS_UNREADING  + \") \")\n    int getUnreadCount(String uid);\n\n    @Query(\"SELECT count(1) FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"AND article.starStatus = \" + App.STATUS_STARED )\n    int getStarCount(String uid);\n\n    @Query(\"SELECT count(1) FROM article \" +\n            \"WHERE uid = :uid \" )\n    int getAllCount(String uid);\n\n    @Query(\"SELECT count(1) FROM article \" +\n            \"LEFT JOIN FeedCategory ON (article.uid = FeedCategory.uid AND article.feedId = FeedCategory.feedId)\" +\n            \"WHERE article.uid = :uid \" +\n            \"AND FeedCategory.categoryId is NULL \" +\n            \"AND (article.readStatus = \" + App.STATUS_UNREAD  + \" OR article.readStatus = \" + App.STATUS_UNREADING  + \")\")\n    int getUncategoryUnreadCount(String uid);\n\n    @Query(\"SELECT count(1) FROM article \" +\n            \"LEFT JOIN FeedCategory ON (article.uid = FeedCategory.uid AND article.feedId = FeedCategory.feedId)\" +\n            \"WHERE article.uid = :uid \" +\n            \"AND FeedCategory.categoryId is NULL \" +\n            \"AND article.starStatus = \" + App.STATUS_STARED)\n    int getUncategoryStarCount(String uid);\n\n    @Query(\"SELECT count(1) FROM article \" +\n            \"LEFT JOIN FeedCategory ON (article.uid = FeedCategory.uid AND article.feedId = FeedCategory.feedId)\" +\n            \"WHERE article.uid = :uid \" +\n            \"AND FeedCategory.categoryId is NULL \")\n    int getUncategoryAllCount(String uid);\n\n    @Query(\"SELECT readUpdated FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"ORDER BY readUpdated DESC LIMIT 1\")\n    long getLastReadTimeMillis(String uid);\n\n    @Query(\"SELECT starUpdated FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"ORDER BY starUpdated DESC LIMIT 1\")\n    long getLastStarTimeMillis(String uid);\n\n    @Query(\"SELECT * FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"AND (article.readStatus = \" + App.STATUS_UNREADING  + \" OR article.saveStatus !=\" + App.STATUS_NOT_FILED + \")\")\n    List<Article> getBackup(String uid);\n\n    // TODO: 2020/5/1 文章要加上是否已经被 Readability 的标志\n    @Query(\"SELECT article.* FROM article \" +\n            \"LEFT JOIN Feed ON (article.uid = Feed.uid AND article.feedId = Feed.id) \" +\n            \"WHERE article.uid = :uid \" +\n            \"AND article.crawlDate = :timeMillis \" +\n            \"AND Feed.displayMode = 1 \")\n    List<Article> getNeedReadability(String uid, long timeMillis);\n\n    @Query(\"SELECT article.* FROM article \" +\n            \"LEFT JOIN ArticleTag ON (article.uid = articletag.uid AND article.id = articletag.articleId) \" +\n            \"WHERE article.uid = :uid \" +\n            \"AND article.crawlDate >= :timeMillis \" +\n            \"AND article.starStatus = \" + App.STATUS_STARED + \" \" +\n            \"AND article.feedId not Null \" +\n            \"AND articletag.tagId is Null \")\n    List<Article> getNotTagStar(String uid, long timeMillis);\n\n    @Query(\"SELECT * FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"AND id in (:ids)\")\n    List<Article> getArticles(String uid, List<String> ids);\n\n    @RawQuery\n    List<Article> getActionRuleArticlesRaw(SupportSQLiteQuery query);\n\n    @RawQuery\n    List<Entry> getActionRuleArticlesRaw2(SupportSQLiteQuery query);\n\n\n\n//    @Query(\"SELECT * FROM article WHERE uid = :uid AND feedId = :feedId \" +\n//            \"AND (readStatus = \" + App.STATUS_UNREAD  + \" OR readStatus = \" + App.STATUS_UNREADING  + \" OR article.starStatus = \" + App.STATUS_STARED  + \") \" +\n//            \"ORDER BY crawlDate DESC,pubDate DESC\")\n//    Cursor getValuableByFeedId(String uid, String feedId);\n\n    @Query(\"SELECT * FROM article WHERE uid = :uid ORDER BY id DESC LIMIT 1\")\n    Article getLastArticle(String uid);\n\n    @Query(\"SELECT link FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"GROUP BY link HAVING COUNT(*) > 1\") //,title\n    List<String> getDuplicatesLink(String uid);\n\n    @Query(\"SELECT * FROM article \" +\n            \"WHERE uid = :uid \" +\n            \"AND link = :link \" +\n            \"ORDER BY crawlDate DESC\")\n    List<Article> getDuplicates(String uid, String link);\n\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    @Transaction\n    void insert(Article... articles);\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    @Transaction\n    void insert(List<Article> articles);\n\n    @Update\n    @Transaction\n    void update(Article... articles);\n\n    @Update\n    @Transaction\n    void update(List<Article> articles);\n\n    /**\n     * 将上次操作之后所有新同步文章的爬取时间都重置\n     * @param uid\n     * @param lastMarkTimeMillis\n     * @param targetTimeMillis\n     */\n    @Query(\"UPDATE Article SET crawlDate = :targetTimeMillis WHERE uid = :uid AND crawlDate > :lastMarkTimeMillis \")\n    void updateIdleCrawlDate(String uid,long lastMarkTimeMillis, long targetTimeMillis);\n\n    @Query(\"DELETE FROM article WHERE uid = :uid AND feedId = :feedId AND starStatus = \" + App.STATUS_UNSTAR)\n    void deleteUnStarByFeedId(String uid, String feedId);\n\n    @Delete\n    @Transaction\n    void delete(Article... articles);\n\n    @Delete\n    @Transaction\n    void delete(List<Article> articles);\n\n    @Query(\"SELECT * FROM article WHERE uid = :uid AND readStatus = \" + App.STATUS_READED + \" AND starStatus = \" + App.STATUS_UNSTAR + \" AND saveStatus = \" + App.STATUS_TO_BE_FILED + \" AND crawlDate < :time\" )\n    List<Article> getReadedUnstarBeFiledLtTime(String uid, long time);\n\n    @Query(\"SELECT * FROM article WHERE uid = :uid AND readStatus = \" + App.STATUS_READED + \" AND starStatus = \" + App.STATUS_STARED + \" AND saveStatus = \" + App.STATUS_TO_BE_FILED + \" AND crawlDate < :time\" )\n    List<Article> getReadedStaredBeFiledLtTime(String uid, long time);\n\n    @Query(\"SELECT * FROM article WHERE uid = :uid AND readStatus = \" + App.STATUS_READED + \" AND starStatus = \" + App.STATUS_UNSTAR + \" AND crawlDate < :time\" )\n    List<Article> getReadedUnstarLtTime(String uid, long time);\n\n    @Query(\"DELETE FROM article WHERE uid = (:uid)\")\n    void clear(String... uid);\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/ArticleTag.java",
    "content": "package me.wizos.loread.db;\n\nimport androidx.annotation.NonNull;\nimport androidx.room.Entity;\nimport androidx.room.ForeignKey;\nimport androidx.room.Index;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport static androidx.room.ForeignKey.CASCADE;\n\n/**\n * Created by Wizos on 2020/3/17.\n */\n@Entity(primaryKeys = {\"uid\",\"articleId\",\"tagId\"},\n        indices = {@Index({\"uid\"}),@Index({\"uid\",\"articleId\"}),@Index({\"uid\",\"tagId\"})},\n        foreignKeys = {@ForeignKey(entity = User.class, parentColumns = \"id\", childColumns = \"uid\", onDelete = CASCADE) }\n)\npublic class ArticleTag {\n    @NonNull\n    private String uid;\n    @NonNull\n    private String articleId;\n    @NonNull\n    private String tagId;\n\n    public ArticleTag(@NonNull String uid, @NonNull String articleId, @NonNull String tagId){\n        this.uid = uid;\n        this.articleId = articleId;\n        this.tagId = tagId;\n    }\n\n    @Override\n    public String toString() {\n        return \"ArticleTag{\" +\n                \"uid=\" + uid +\n                \", articleId='\" + articleId + '\\'' +\n                \", tagId='\" + tagId + '\\'' +\n                '}';\n    }\n\n    @NonNull\n    public String getUid() {\n        return uid;\n    }\n\n    public void setUid(@NonNull String uid) {\n        this.uid = uid;\n    }\n\n    @NotNull\n    public String getTagId() {\n        return this.tagId;\n    }\n\n\n    public void setTagId(@NotNull String tagId) {\n        this.tagId = tagId;\n    }\n\n\n    @NotNull\n    public String getArticleId() {\n        return this.articleId;\n    }\n\n    public void setArticleId(@NotNull String articleId) {\n        this.articleId = articleId;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/ArticleTagDao.java",
    "content": "package me.wizos.loread.db;\n\nimport androidx.room.Dao;\nimport androidx.room.Delete;\nimport androidx.room.Insert;\nimport androidx.room.OnConflictStrategy;\nimport androidx.room.Query;\nimport androidx.room.Transaction;\nimport androidx.room.Update;\n\nimport java.util.List;\n\n@Dao\npublic interface ArticleTagDao {\n    @Query(\"SELECT * FROM articletag WHERE uid = :uid\")\n    List<ArticleTag> getAll(String uid);\n\n    @Query(\"SELECT * FROM articletag WHERE uid = :uid AND articleId = :articleId\")\n    List<ArticleTag> getByArticleId(String uid, String articleId);\n\n    @Query(\"SELECT * FROM articletag WHERE uid = :uid AND tagId = :tagId\")\n    List<ArticleTag> getByTagId(String uid, String tagId);\n\n    @Query(\"SELECT articletag.* FROM articletag \" +\n            \"LEFT JOIN Article ON (articletag.uid = article.uid AND articletag.articleId = article.id) \" +\n            \"WHERE articletag.uid = :uid \" +\n            \"AND tagId is Null\")\n    List<ArticleTag> getNotArticles(String uid);\n\n\n    @Query(\"SELECT count(*) FROM articletag WHERE uid = :uid AND tagId = :tagId\")\n    int getCountByTagId(String uid, String tagId);\n\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    @Transaction\n    void insert(ArticleTag... articleTags);\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    @Transaction\n    void insert(List<ArticleTag> feedCategories);\n\n    @Update\n    @Transaction\n    void update(ArticleTag... feedCategories);\n\n    @Query(\"UPDATE articletag SET tagId = :newTagId where  uid = :uid AND tagId = :oldTagId\")\n    void updateCategoryId(String uid, String oldTagId, String newTagId);\n\n\n    @Delete\n    @Transaction\n    void delete(ArticleTag articleTag);\n\n    @Delete\n    @Transaction\n    void delete(List<ArticleTag> articleTags);\n\n    @Query(\"DELETE FROM articletag WHERE uid = :uid AND articleId = :articleId\")\n    void deleteByArticleId(String uid, String articleId);\n\n    @Query(\"DELETE FROM articletag WHERE uid = (:uid) AND tagId = :tagId\")\n    void deleteByTagId(String uid, String tagId);\n\n    @Query(\"DELETE FROM articletag WHERE uid = (:uid)\")\n    void clear(String... uid);\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/Category.java",
    "content": "package me.wizos.loread.db;\n\nimport androidx.annotation.NonNull;\nimport androidx.room.Entity;\nimport androidx.room.ForeignKey;\nimport androidx.room.Ignore;\nimport androidx.room.Index;\n\nimport me.wizos.loread.bean.feedly.CategoryItem;\n\nimport static androidx.room.ForeignKey.CASCADE;\n\n/**\n indices = {@Index({\"id\",\"uid\",\"title\"})} ,\n\n * Created by Wizos on 2020/3/17.\n */\n\n@Entity(primaryKeys = {\"id\",\"uid\"},\n        indices = {@Index({\"id\"}),@Index({\"uid\"}),@Index({\"title\"})},\n        foreignKeys = @ForeignKey(entity = User.class, parentColumns = \"id\", childColumns = \"uid\", onDelete = CASCADE))\npublic class Category {\n    @NonNull\n    private String id;\n    @NonNull\n    private String uid;\n    private String title;\n\n    private int unreadCount;\n    private int starCount;\n    private int allCount;\n    // 添加此标记后不会生成数据库表的列\n    @Ignore\n    public boolean isExpand;\n\n    public String getUid() {\n        return uid;\n    }\n\n    public void setUid(String uid) {\n        this.uid = uid;\n    }\n\n    public String getId() {\n        return this.id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getTitle() {\n        return this.title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    public int getUnreadCount() {\n        return this.unreadCount;\n    }\n\n    public void setUnreadCount(int unreadCount) {\n        this.unreadCount = unreadCount;\n    }\n\n    public int getStarCount() {\n        return this.starCount;\n    }\n\n    public void setStarCount(int starCount) {\n        this.starCount = starCount;\n    }\n\n    public int getAllCount() {\n        return this.allCount;\n    }\n\n    public void setAllCount(int allCount) {\n        this.allCount = allCount;\n    }\n\n    public CategoryItem convert2CategoryItem() {\n        CategoryItem category = new CategoryItem();\n        category.setId(id);\n        category.setLabel(title);\n        return category;\n    }\n\n    @Override\n    public String toString() {\n        return \"Category{\" +\n                \"id='\" + id + '\\'' +\n                \", uid='\" + uid + '\\'' +\n                \", title='\" + title + '\\'' +\n                \", unreadCount=\" + unreadCount +\n                \", starCount=\" + starCount +\n                \", allCount=\" + allCount +\n                \", isExpand=\" + isExpand +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/CategoryDao.java",
    "content": "package me.wizos.loread.db;\n\nimport androidx.lifecycle.LiveData;\nimport androidx.room.Dao;\nimport androidx.room.Delete;\nimport androidx.room.Insert;\nimport androidx.room.OnConflictStrategy;\nimport androidx.room.Query;\nimport androidx.room.Transaction;\nimport androidx.room.Update;\n\nimport java.util.List;\n\n@Dao\npublic interface CategoryDao {\n    @Query(\"SELECT * FROM category WHERE uid = :uid ORDER BY title COLLATE NOCASE ASC\")\n    List<Category> getAll(String uid);\n\n\n\n    @Query(\"SELECT id,title,unreadCount as count FROM category WHERE uid = :uid ORDER BY title COLLATE NOCASE ASC\")\n    List<Collection> getCategoriesUnreadCount(String uid);\n\n    @Query(\"SELECT id,title,starCount as count FROM category WHERE uid = :uid ORDER BY title COLLATE NOCASE ASC\")\n    List<Collection> getCategoriesStarCount(String uid);\n\n    @Query(\"SELECT id,title,allCount as count FROM category WHERE uid = :uid ORDER BY title COLLATE NOCASE ASC\")\n    List<Collection> getCategoriesAllCount(String uid);\n\n\n    @Query(\"SELECT * FROM category WHERE uid = :uid ORDER BY title COLLATE NOCASE ASC\")\n    LiveData<List<Category>> getAllLiveData(String uid);\n\n\n    @Query(\"SELECT category.* FROM category \" +\n            \"LEFT JOIN feedcategory ON (category.uid = feedcategory.uid AND category.id = feedcategory.categoryId) \" +\n            \"WHERE category.uid = :uid AND FeedCategory.feedId = :feedId \" +\n            \"ORDER BY title COLLATE NOCASE ASC\")\n    List<Category> getByFeedId(String uid, String feedId);\n\n    @Query(\"SELECT * FROM category WHERE uid = :uid AND id = :id LIMIT 1\")\n    Category getById(String uid, String id);\n\n    @Query(\"SELECT * FROM categoryview WHERE uid = :uid\" )\n    List<Category> getCategoriesRealTimeCount(String uid);\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    @Transaction\n    void insert(Category... categories);\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    @Transaction\n    void insert(List<Category> categories);\n\n    @Query(\"UPDATE category SET id = :newId where uid = :uid AND id = :oldId\")\n    void updateId(String uid, String oldId, String newId);\n\n    @Update\n    @Transaction\n    void update(Category... categories);\n\n    @Update\n    @Transaction\n    void update(List<Category> categories);\n\n    @Delete\n    @Transaction\n    void delete(Category... categories);\n\n    @Query(\"DELETE FROM category WHERE uid = (:uid)\")\n    void clear(String... uid);\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/CategoryView.java",
    "content": "package me.wizos.loread.db;\n\nimport androidx.room.DatabaseView;\n\n/**\n indices = {@Index({\"id\",\"uid\",\"title\"})} ,\n * Created by Wizos on 2020/3/17.\n */\n\n@DatabaseView(\n        \"SELECT CATEGORY.uid,id,title,UNREAD_SUM AS unreadCount,STAR_SUM AS starCount,ALL_SUM AS allCount FROM CATEGORY \" +\n                \"LEFT JOIN \" +\n                \"(\" +\n                \"\tSELECT uid,categoryId,sum(UNREAD_SUM) AS UNREAD_SUM,sum(STAR_SUM) AS STAR_SUM,sum(ALL_SUM) AS ALL_SUM FROM \" +\n                \"\t(\" +\n                \"\t\tSELECT FEEDCATEGORY.uid,categoryId,feedId,UNREAD_SUM,STAR_SUM,ALL_SUM FROM FEEDCATEGORY \" +\n                \"\t\tLEFT JOIN (SELECT uid,id,unreadCount AS UNREAD_SUM, starCount AS STAR_SUM, allCount AS ALL_SUM FROM feedview) AS FEED ON feedcategory.uid = FEED.uid AND feedcategory.feedId = FEED.ID \" +\n                \"\t) AS FeedCategoryCount GROUP BY uid,CATEGORYID \" +\n                \") AS C ON CATEGORY.uid = C.uid AND CATEGORY.ID = C.CATEGORYID \"\n)\npublic class CategoryView extends Category{\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/Collection.java",
    "content": "package me.wizos.loread.db;\n\nimport me.wizos.loread.bean.feedly.CategoryItem;\n\n// Category 和 Feed 的抽象\npublic class Collection {\n    private String id;\n    private String title;\n    private int count;\n\n    public String getId() {\n        return this.id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getTitle() {\n        return this.title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    public int getCount() {\n        return this.count;\n    }\n\n    public void setCount(int count) {\n        this.count = count;\n    }\n\n    public CategoryItem convert2CategoryItem() {\n        CategoryItem category = new CategoryItem();\n        category.setId(id);\n        category.setLabel(title);\n        return category;\n    }\n\n    @Override\n    public String toString() {\n        return \"Collection{\" +\n                \"id='\" + id + '\\'' +\n                \", title='\" + title + '\\'' +\n                \", count=\" + count +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/CoreDB.java",
    "content": "package me.wizos.loread.db;\n\nimport android.content.Context;\n\nimport androidx.annotation.NonNull;\nimport androidx.room.Database;\nimport androidx.room.Room;\nimport androidx.room.RoomDatabase;\nimport androidx.sqlite.db.SupportSQLiteDatabase;\n\nimport com.socks.library.KLog;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.bean.feedly.CategoryItem;\nimport me.wizos.loread.bean.feedly.input.EditFeed;\n\n/**\n * @Database标签用于告诉系统这是Room数据库对象。\n * entities属性用于指定该数据库有哪些表，若需建立多张表，以逗号相隔开。\n * version属性用于指定数据库版本号，后续数据库的升级正是依据版本号来判断的。\n * 该类需要继承自RoomDatabase，在类中，通过Room.databaseBuilder()结合单例设计模式，完成数据库的创建工作。\n */\n@Database(\n        entities = {User.class,Article.class,Feed.class,Category.class, FeedCategory.class, Tag.class, ArticleTag.class},\n        views = {FeedView.class,CategoryView.class},\n        version = 1,\n        exportSchema = false\n)\npublic abstract class CoreDB extends RoomDatabase {\n    private static final String DATABASE_NAME = \"loreadx.db\";\n    private static CoreDB databaseInstance;\n\n    public static synchronized void init(Context context) {\n        if(databaseInstance == null) {\n            synchronized (CoreDB.class) { // 同步锁，避免多线程时可能 new 出两个实例的情况\n                if (databaseInstance == null) {\n                    databaseInstance = Room.databaseBuilder(context.getApplicationContext(), CoreDB.class, DATABASE_NAME)\n                            .addCallback(new Callback() {\n                                @Override\n                                public void onOpen(@NonNull SupportSQLiteDatabase db) {\n                                    super.onOpen(db);\n                                    KLog.e(\"创建触发器\");\n                                    createTriggers(db);\n                                }\n                            })\n//                            .addMigrations(MIGRATION_1_2)\n                            .allowMainThreadQueries()\n                            .build();\n                }\n            }\n        }\n    }\n\n    public static CoreDB i(){\n        if( databaseInstance == null ){\n            throw new RuntimeException(\"CoreBD must init in Application class\");\n        }\n        return databaseInstance;\n    }\n//\n//    static final Migration MIGRATION_1_2 = new Migration(1, 2) {\n//        @Override\n//        public void migrate(SupportSQLiteDatabase database) {\n//            //因为修改了视图所以先删除老的视图\n////            database.execSQL(\"drop view FeedView\");\n////            database.execSQL(\"drop view CategoryView;\");\n//        }\n//    };\n\n    /**\n     * 创建触发器\n     * 在 INSERT 型触发器中，只有NEW是合法的，NEW 用来表示将要（BEFORE）或已经（AFTER）插入的新数据；\n     * 在 UPDATE 型触发器中，NEW、OLD可以同时使用，OLD 用来表示将要或已经被修改的原数据，NEW 用来表示将要或已经修改为的新数据；\n     * 在 DELETE 型触发器中，只有 OLD 才合法，OLD 用来表示将要或已经被删除的原数据；\n     * 使用方法： NEW.columnName （columnName 为相应数据表某一列名）\n     * 另外，OLD 是只读的，而 NEW 则可以在触发器中使用 SET 赋值，这样不会再次触发触发器，造成循环调用（如每插入一个学生前，都在其学号前加“2013”）。\n     * @param db\n     */\n    private static void createTriggers(SupportSQLiteDatabase db) {\n        //【当插入文章时】\n        String updateFeedAllCountWhenInsertArticle =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updateFeedAllCountWhenInsertArticle\" +\n                        \" AFTER INSERT ON ARTICLE\" +\n                        \"  BEGIN\" +\n                        \"   UPDATE FEED SET ALLCOUNT = ALLCOUNT + 1 WHERE ID IS new.FEEDID AND UID IS new.UID;\" +\n                        \"  END\";\n        db.execSQL(updateFeedAllCountWhenInsertArticle);\n        String updateFeedUnreadCountWhenInsertArticle =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updateFeedUnreadCountWhenInsertArticle\" +\n                        \" AFTER INSERT ON ARTICLE\" +\n                        \" WHEN (new.READSTATUS = 1)\" +\n                        \"   BEGIN\" +\n                        \"        UPDATE FEED SET UNREADCOUNT = UNREADCOUNT + 1 WHERE ID IS new.FEEDID AND UID IS new.UID;\" +\n                        \"   END\" ;\n        db.execSQL(updateFeedUnreadCountWhenInsertArticle);\n        String updateFeedStarCountWhenInsertArticle =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updateFeedStarCountWhenInsertArticle\" +\n                        \" AFTER INSERT ON ARTICLE\" +\n                        \" WHEN (new.STARSTATUS = 4)\" +\n                        \"      BEGIN\" +\n                        \"        UPDATE FEED SET STARCOUNT = STARCOUNT + 1 WHERE ID IS new.FEEDID AND UID IS new.UID;\" +\n                        \"      END\";\n        db.execSQL(updateFeedStarCountWhenInsertArticle);\n\n\n\n        //【当删除文章时】\n        String updateFeedAllCountWhenDeleteArticle =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updateFeedAllCountWhenDeleteArticle\" +\n                        \"  AFTER DELETE ON ARTICLE\" +\n                        \"    BEGIN\" +\n                        \"        UPDATE FEED SET ALLCOUNT = ALLCOUNT - 1 WHERE ID IS old.FEEDID AND UID IS old.UID;\" +\n                        \"    END\";\n        db.execSQL(updateFeedAllCountWhenDeleteArticle);\n        String updateFeedUnreadCountWhenDeleteArticle =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updateFeedUnreadCountWhenDeleteArticle\" +\n                        \" AFTER DELETE ON ARTICLE\" +\n                        \" WHEN (old.READSTATUS = 1 OR old.READSTATUS = 3)\" +\n                        \"   BEGIN\" +\n                        \"        UPDATE FEED SET UNREADCOUNT = UNREADCOUNT - 1 WHERE ID IS old.FEEDID AND UID IS old.UID;\" +\n                        \"   END\" ;\n        db.execSQL(updateFeedUnreadCountWhenDeleteArticle);\n        String updateFeedStarCountWhenDeleteArticle =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updateFeedStarCountWhenDeleteArticle\" +\n                        \" AFTER DELETE ON ARTICLE\" +\n                        \" WHEN (old.STARSTATUS = 4)\" +\n                        \"      BEGIN\" +\n                        \"        UPDATE FEED SET STARCOUNT = STARCOUNT - 1 WHERE ID IS old.FEEDID AND UID IS old.UID;\" +\n                        \"      END\";\n        db.execSQL(updateFeedStarCountWhenDeleteArticle);\n\n//        // 当 READSTATUS 状态更新时，标注其更新时间\n//        String updatedWhenArticleChange =\n//                \"CREATE TEMP TRIGGER IF NOT EXISTS updatedWhenArticleChange\" +\n//                        \" AFTER UPDATE OF READSTATUS,STARSTATUS ON ARTICLE\" +\n//                        \" WHEN old.READSTATUS != new.READSTATUS OR old.STARSTATUS != new.STARSTATUS\" +\n//                        \" BEGIN\" +\n//                        \"  UPDATE ARTICLE SET updateTime = (strftime('%s','now') || substr(strftime('%f','now'),4))\" +\n//                        \"  WHERE UID IS old.UID AND ID IS old.ID;\" +\n//                        \" END\";\n//        db.execSQL(updatedWhenArticleChange);\n\n        // 当 READSTATUS 状态更新时，标注其更新时间\n        String updatedWhenReadStatusChange =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updatedWhenReadStatusChange\" +\n                        \" AFTER UPDATE OF READSTATUS ON ARTICLE\" +\n                        \" WHEN old.READSTATUS != new.READSTATUS\" +\n                        \" BEGIN\" +\n                        \"  UPDATE ARTICLE SET readUpdated = (strftime('%s','now') || substr(strftime('%f','now'),4))\" +\n                        \"  WHERE ID IS old.ID AND UID IS old.UID;\" +\n                        \" END\";\n        db.execSQL(updatedWhenReadStatusChange);\n        // 当 STARSTATUS 状态更新时，标注其更新时间\n        String updatedWhenStarStatusChange =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updatedWhenStarStatusChange\" +\n                        \" AFTER UPDATE OF STARSTATUS ON ARTICLE\" +\n                        \" WHEN old.STARSTATUS != new.STARSTATUS\" +\n                        \" BEGIN\" +\n                        \"  UPDATE ARTICLE SET starUpdated = (strftime('%s','now') || substr(strftime('%f','now'),4))\" +\n                        \"  WHERE ID IS old.ID AND UID IS old.UID;\" +\n                        \" END\";\n        db.execSQL(updatedWhenStarStatusChange);\n\n\n        // 当updateTime因为\n        String readUpdatedNoChange =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS readUpdatedNoChange\" +\n                        \" AFTER UPDATE OF readUpdated ON ARTICLE\" +\n                        \" WHEN old.readUpdated > new.readUpdated\" +\n                        \" BEGIN\" +\n                        \"  UPDATE ARTICLE SET readUpdated = old.readUpdated\" +\n                        \"  WHERE UID IS old.UID AND ID IS old.ID;\" +\n                        \" END\";\n        db.execSQL(readUpdatedNoChange);\n\n        // 当updateTime因为\n        String starUpdatedNoChange =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS starUpdatedNoChange\" +\n                        \" AFTER UPDATE OF starUpdated ON ARTICLE\" +\n                        \" WHEN old.starUpdated > new.starUpdated\" +\n                        \" BEGIN\" +\n                        \"  UPDATE ARTICLE SET starUpdated = old.starUpdated\" +\n                        \"  WHERE UID IS old.UID AND ID IS old.ID;\" +\n                        \" END\";\n        db.execSQL(starUpdatedNoChange);\n\n\n        // 标记文章为已读时，更新feed的未读计数\n        String updateFeedUnreadCountWhenReadArticle =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updateFeedUnreadCountWhenReadArticle\" +\n                        \" AFTER UPDATE OF READSTATUS ON ARTICLE\" +\n                        \" WHEN (old.READSTATUS != 2 AND new.READSTATUS = 2)\" +\n                        \" BEGIN\" +\n                        \"  UPDATE FEED SET UNREADCOUNT = UNREADCOUNT - 1 WHERE ID IS old.FEEDID AND UID IS old.UID;\" +\n                        \" END\";\n        db.execSQL(updateFeedUnreadCountWhenReadArticle);\n        // 标记文章为未读时，更新feed的未读计数\n        String updateFeedUnreadCountWhenUnreadArticle =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updateFeedUnreadCountWhenUnreadArticle\" +\n                        \"      AFTER UPDATE OF READSTATUS ON ARTICLE\" +\n                        \"      WHEN (old.READSTATUS = 2 AND new.READSTATUS != 2 )\" +\n                        \"      BEGIN\" +\n                        \"        UPDATE FEED\" +\n                        \"          SET UNREADCOUNT = UNREADCOUNT + 1\" +\n                        \"          WHERE ID IS old.FEEDID AND UID IS old.UID;\" +\n                        \"      END\";\n        db.execSQL(updateFeedUnreadCountWhenUnreadArticle);\n\n        // 标记文章为加星时，更新feed的计数\n        String updateFeedUnreadCountWhenStaredArticle =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updateFeedUnreadCountWhenStaredArticle\" +\n                        \"      AFTER UPDATE OF STARSTATUS ON ARTICLE\" +\n                        \"      WHEN (old.STARSTATUS != 4 AND new.STARSTATUS = 4)\" +\n                        \"      BEGIN\" +\n                        \"        UPDATE FEED SET STARCOUNT = STARCOUNT + 1\" +\n                        \"        WHERE ID IS old.FEEDID AND UID IS old.UID;\" +\n                        \"      END\";\n        db.execSQL(updateFeedUnreadCountWhenStaredArticle);\n        // 标记文章为无星时，更新feed的计数\n        String updateFeedUnreadCountWhenUnstarArticle =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updateFeedUnreadCountWhenUnstarArticle\" +\n                        \"      AFTER UPDATE OF STARSTATUS ON ARTICLE\" +\n                        \"      WHEN (old.STARSTATUS != 5 AND new.STARSTATUS = 5)\" +\n                        \"      BEGIN\" +\n                        \"        UPDATE FEED SET STARCOUNT = STARCOUNT - 1\" +\n                        \"        WHERE ID IS old.FEEDID AND UID IS old.UID;\" +\n                        \"      END\";\n        db.execSQL(updateFeedUnreadCountWhenUnstarArticle);\n\n\n        // 当feed的总计数加一时，更新tag的总计数\n        String updateTagAllCountWhenInsertArticle =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updateTagAllCountWhenInsertArticle\" +\n                        \" AFTER UPDATE OF ALLCOUNT ON FEED\" +\n                        \" WHEN (new.ALLCOUNT = old.ALLCOUNT + 1)\" +\n                        \"   BEGIN\" +\n                        \"        UPDATE CATEGORY SET ALLCOUNT = ALLCOUNT + 1\" +\n                        \"        WHERE ID IN (select CATEGORYID from FEEDCATEGORY where FEEDID = old.ID AND UID IS old.UID);\" +\n                        \"   END\";\n        db.execSQL(updateTagAllCountWhenInsertArticle);\n        // 当feed的总计数减一时，更新tag的总计数\n        String updateTagAllCountWhenDeleteArticle =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updateTagAllCountWhenDeleteArticle\" +\n                        \" AFTER UPDATE OF ALLCOUNT ON FEED\" +\n                        \" WHEN (new.ALLCOUNT = old.ALLCOUNT - 1)\" +\n                        \"      BEGIN\" +\n                        \"        UPDATE CATEGORY SET ALLCOUNT = ALLCOUNT - 1\" +\n                        \"        WHERE ID IN (select CATEGORYID from FEEDCATEGORY where FEEDID = old.ID AND UID IS old.UID);\" +\n                        \"      END\";\n        db.execSQL(updateTagAllCountWhenDeleteArticle);\n\n\n        // 当feed的未读计数加一时，更新tag的未读计数\n        String updateTagUnreadCountWhenAdd =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updateTagUnreadCountWhenAdd\" +\n                        \" AFTER UPDATE OF UNREADCOUNT ON FEED\" +\n                        \" WHEN (new.UNREADCOUNT = old.UNREADCOUNT + 1)\" +\n                        \"      BEGIN\" +\n                        \"        UPDATE CATEGORY\" +\n                        \"          SET UNREADCOUNT = UNREADCOUNT + 1\" +\n                        \"          WHERE ID IN (select CATEGORYID from FEEDCATEGORY where FEEDID = old.ID AND UID IS old.UID);\" +\n                        \"      END\";\n        db.execSQL(updateTagUnreadCountWhenAdd);\n        // 当feed的未读计数减一时，更新tag的未读计数\n        String updateTagUnreadCountWhenMinus =\n                \"CREATE TEMP TRIGGER IF NOT EXISTS updateTagUnreadCountWhenMinus\" +\n                        \"      AFTER UPDATE OF UNREADCOUNT ON FEED\" +\n                        \"      WHEN (new.UNREADCOUNT = old.UNREADCOUNT - 1)\" +\n                        \"      BEGIN\" +\n                        \"        UPDATE CATEGORY\" +\n                        \"          SET UNREADCOUNT = UNREADCOUNT - 1\" +\n                        \"          WHERE ID IN (select CATEGORYID from FEEDCATEGORY where FEEDID = old.ID AND UID IS old.UID);\" +\n                        \"      END\";\n        db.execSQL(updateTagUnreadCountWhenMinus);\n\n\n        // 当feed的星标计数加一时，更新tag的星标计数\n        String updateTagStarCountWhenAdd =\n                \"    CREATE TEMP TRIGGER IF NOT EXISTS updateTagStarCountWhenAdd\" +\n                        \" AFTER UPDATE OF STARCOUNT ON FEED\" +\n                        \" WHEN (new.STARCOUNT = old.STARCOUNT + 1)\" +\n                        \"      BEGIN\" +\n                        \"        UPDATE CATEGORY SET STARCOUNT = STARCOUNT + 1\" +\n                        \"        WHERE ID IN (select CATEGORYID from FEEDCATEGORY where FEEDID = old.ID AND UID IS old.UID);\" +\n                        \"      END\";\n        db.execSQL(updateTagStarCountWhenAdd);\n\n        String updateTagStarCountWhenMinus =\n                \"    CREATE TEMP TRIGGER IF NOT EXISTS updateTagStarCountWhenMinus\" +\n                        \" AFTER UPDATE OF STARCOUNT ON FEED\" +\n                        \" WHEN (new.STARCOUNT = old.STARCOUNT - 1)\" +\n                        \"      BEGIN\" +\n                        \"        UPDATE CATEGORY SET STARCOUNT = STARCOUNT - 1\" +\n                        \"        WHERE ID IN (select CATEGORYID from FEEDCATEGORY where FEEDID = old.ID AND UID IS old.UID);\" +\n                        \"      END\";\n        db.execSQL(updateTagStarCountWhenMinus);\n    }\n\n\n    /**\n     * 我们创建的Dao对象，在这里以抽象方法的形式返回，只需一行代码即可。\n     */\n    public abstract UserDao userDao();\n    public abstract ArticleDao articleDao();\n    public abstract FeedDao feedDao();\n    public abstract CategoryDao categoryDao();\n    public abstract FeedCategoryDao feedCategoryDao();\n    //public abstract ArticleFtsDao articleFtsDao();\n\n    public abstract TagDao tagDao();\n    public abstract ArticleTagDao articleTagDao();\n\n    public void coverFeedCategories(EditFeed editFeed) {\n        String uid = App.i().getUser().getId();\n        List<CategoryItem> cloudyCategoryItems = editFeed.getCategoryItems();\n        ArrayList<FeedCategory> feedCategories = new ArrayList<>(cloudyCategoryItems.size());\n        FeedCategory feedCategory;\n        for (CategoryItem categoryItem : cloudyCategoryItems) {\n            feedCategory = new FeedCategory(uid, editFeed.getId(), categoryItem.getId());\n            feedCategories.add(feedCategory);\n        }\n\n        CoreDB.i().feedCategoryDao().deleteByFeedId(uid, editFeed.getId());\n        CoreDB.i().feedCategoryDao().insert(feedCategories);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/CorePref.java",
    "content": "package me.wizos.loread.db;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.SharedPreferences;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\n\n/**\n * @author Wizos\n * @date 2016/4/30\n * 内部设置\n */\npublic class CorePref {\n    private static final String TAG = \"CorePref\";\n    private static CorePref coreSharedPreferences;\n    private static SharedPreferences mySharedPreferences;\n    private static SharedPreferences.Editor editor;\n    private CorePref() {\n    }\n\n    @SuppressLint(\"CommitPrefEdits\")\n    public static CorePref i() {\n        // 双重锁定，只有在 mySharedPreferences 还没被初始化的时候才会进入到下一行，然后加上同步锁\n        if (coreSharedPreferences == null) {\n            // 同步锁，避免多线程时可能 new 出两个实例的情况\n            synchronized (CorePref.class) {\n                if (coreSharedPreferences == null) {\n                    coreSharedPreferences = new CorePref();\n                    mySharedPreferences = App.i().getSharedPreferences(App.i().getString(R.string.app_id), Activity.MODE_PRIVATE);\n                    editor = mySharedPreferences.edit();\n                }\n            }\n        }\n        return coreSharedPreferences;\n    }\n\n    public String getString(String key, String value) {\n        return mySharedPreferences.getString(key, value);\n    }\n    public void putString(String key, String value){\n        editor.putString(key, value); //用putString的方法保存数据\n        editor.commit(); //提交当前数据\n    }\n    public void remove(String key){\n        editor.remove(key).commit();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/Entry.java",
    "content": "package me.wizos.loread.db;\n\npublic class Entry {\n    private String id;\n    private String entry;\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getEntry() {\n        return entry;\n    }\n\n    public void setEntry(String entry) {\n        this.entry = entry;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/Feed.java",
    "content": "package me.wizos.loread.db;\n\nimport androidx.annotation.NonNull;\nimport androidx.room.Entity;\nimport androidx.room.ForeignKey;\nimport androidx.room.Index;\n\nimport static androidx.room.ForeignKey.CASCADE;\n\n/**\n * Feed 与 Category 是 多对多关系，即一个 Feed 可以存在与多个 Category 中，Category 也可以包含多个 Feed\n * Created by Wizos on 2020/3/17.\n // @Index({\"id\", \"uid\", \"title\", \"feedUrl\"})\n @Index({\"id\"}),@Index({\"uid\"}),\n */\n@Entity(\n        primaryKeys = {\"id\",\"uid\"},\n        indices = {@Index({\"id\"}),@Index({\"uid\"}),@Index({\"title\"}),@Index({\"feedUrl\"})},\n        foreignKeys = @ForeignKey(entity = User.class, parentColumns = \"id\", childColumns = \"uid\", onDelete = CASCADE) )\npublic class Feed {\n    @NonNull\n    private String id;\n    @NonNull\n    private String uid;\n\n    private String title;\n\n    private String feedUrl;\n    private String htmlUrl;\n    private String iconUrl;\n\n    // 0->rss, 1->readability, 2->link\n    private int displayMode;\n\n    private int unreadCount;\n    private int starCount;\n    private int allCount;\n\n    // 记录该文feed什么时候被取消订阅。0为已订阅\n    private long state = 0;\n\n    public String getUid() {\n        return uid;\n    }\n\n    public void setUid(String uid) {\n        this.uid = uid;\n    }\n\n    public String getId() {\n        return this.id;\n    }\n\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n\n    public String getTitle() {\n        return this.title;\n    }\n\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n\n    public String getFeedUrl() {\n        return this.feedUrl;\n    }\n\n\n    public void setFeedUrl(String feedUrl) {\n        this.feedUrl = feedUrl;\n    }\n\n\n    public String getHtmlUrl() {\n        return this.htmlUrl;\n    }\n\n\n    public void setHtmlUrl(String htmlUrl) {\n        this.htmlUrl = htmlUrl;\n    }\n\n\n    public String getIconUrl() {\n        return this.iconUrl;\n    }\n\n\n    public void setIconUrl(String iconUrl) {\n        this.iconUrl = iconUrl;\n    }\n\n    // 0->rss, 1->readability, 2->link\n    public int getDisplayMode() {\n        return this.displayMode;\n    }\n\n\n    public void setDisplayMode(int displayMode) {\n        this.displayMode = displayMode;\n    }\n\n\n    public int getUnreadCount() {\n        return this.unreadCount;\n    }\n\n\n    public void setUnreadCount(int unreadCount) {\n        this.unreadCount = unreadCount;\n    }\n\n\n    public int getStarCount() {\n        return this.starCount;\n    }\n\n\n    public void setStarCount(int starCount) {\n        this.starCount = starCount;\n    }\n\n\n    public int getAllCount() {\n        return this.allCount;\n    }\n\n\n    public void setAllCount(int allCount) {\n        this.allCount = allCount;\n    }\n\n\n    public long getState() {\n        return this.state;\n    }\n\n\n    public void setState(long state) {\n        this.state = state;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/FeedCategory.java",
    "content": "package me.wizos.loread.db;\n\nimport androidx.annotation.NonNull;\nimport androidx.room.Entity;\nimport androidx.room.ForeignKey;\nimport androidx.room.Index;\n\nimport static androidx.room.ForeignKey.CASCADE;\n\n/**\n * Created by Wizos on 2020/3/17.\n */\n@Entity(primaryKeys = {\"uid\",\"categoryId\",\"feedId\"},\n        indices = {@Index({\"uid\"}),@Index({\"categoryId\",\"uid\"}),@Index({\"feedId\",\"uid\"})},\n        foreignKeys = {\n                @ForeignKey(entity = User.class, parentColumns = \"id\", childColumns = \"uid\", onDelete = CASCADE)\n//                @ForeignKey(entity = Feed.class, parentColumns = {\"id\",\"uid\"}, childColumns = {\"feedId\",\"uid\"}, onDelete = CASCADE),\n//                @ForeignKey(entity = Category.class, parentColumns = {\"id\",\"uid\"}, childColumns = {\"categoryId\",\"uid\"}, onDelete = CASCADE)\n        }\n)\npublic class FeedCategory {\n    @NonNull\n    private String uid;\n    @NonNull\n    private String feedId;\n    @NonNull\n    private String categoryId;\n\n    public FeedCategory(@NonNull String uid, @NonNull String feedId, @NonNull String categoryId){\n        this.uid = uid;\n        this.feedId = feedId;\n        this.categoryId = categoryId;\n\n//        if( feedId.startsWith(\"feed/\") ){\n//            this.feedId = feedId;\n//        }else {\n//            this.feedId = \"feed/\" + feedId;\n//        }\n//\n//        if( categoryId.startsWith(\"user/\")){\n//            this.categoryId = categoryId;\n//        }else {\n//            this.categoryId = \"user/\" + categoryId;\n//        }\n    }\n\n    @Override\n    public String toString() {\n        return \"FeedCategory{\" +\n                \"uid=\" + uid +\n                \", categoryId='\" + categoryId + '\\'' +\n                \", feedId='\" + feedId + '\\'' +\n                '}';\n    }\n\n    @NonNull\n    public String getUid() {\n        return uid;\n    }\n\n    public void setUid(@NonNull String uid) {\n        this.uid = uid;\n    }\n\n    public String getId() {\n        return this.uid;\n    }\n\n\n    public void setId(String uid) {\n        this.uid = uid;\n    }\n\n\n    public String getCategoryId() {\n        return this.categoryId;\n    }\n\n\n    public void setCategoryId(String categoryId) {\n        if( !categoryId.startsWith(\"user/\")){\n            this.categoryId  = \"user/\" + categoryId;\n        }else {\n            this.categoryId = categoryId;\n        }\n    }\n\n\n    public String getFeedId() {\n        return this.feedId;\n    }\n\n\n    public void setFeedId(String feedId) {\n        if( !feedId.startsWith(\"feed/\") ){\n            this.feedId = \"feed/\" + feedId;\n        }else {\n            this.feedId = feedId;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/FeedCategoryDao.java",
    "content": "package me.wizos.loread.db;\n\nimport androidx.room.Dao;\nimport androidx.room.Delete;\nimport androidx.room.Insert;\nimport androidx.room.OnConflictStrategy;\nimport androidx.room.Query;\nimport androidx.room.Transaction;\nimport androidx.room.Update;\n\nimport java.util.List;\n\n@Dao\npublic interface FeedCategoryDao {\n    @Query(\"SELECT * FROM feedcategory WHERE uid = :uid\")\n    List<FeedCategory> getAll(String uid);\n\n    @Query(\"SELECT * FROM feedcategory WHERE uid = :uid AND categoryId = :categoryId\")\n    List<FeedCategory> getByCategoryId(String uid,String categoryId);\n\n    @Query(\"SELECT count(*) FROM feedcategory WHERE uid = :uid AND categoryId = :categoryId\")\n    int getCountByCategoryId(String uid,String categoryId);\n\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    @Transaction\n    void insert(FeedCategory... feedCategories);\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    @Transaction\n    void insert(List<FeedCategory> feedCategories);\n\n    @Update\n    @Transaction\n    void update(FeedCategory... feedCategories);\n\n    @Query(\"UPDATE feedcategory SET categoryId = :newCategoryId where  uid = :uid AND categoryId = :oldCategoryId\")\n    void updateCategoryId(String uid,String oldCategoryId, String newCategoryId);\n\n\n    @Delete\n    @Transaction\n    void delete(FeedCategory feedCategory);\n\n    @Query(\"DELETE FROM feedcategory WHERE uid = (:uid) AND feedId = :feedId\")\n    void deleteByFeedId(String uid, String feedId);\n\n    @Query(\"DELETE FROM feedcategory WHERE uid = (:uid)\")\n    void clear(String... uid);\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/FeedDao.java",
    "content": "package me.wizos.loread.db;\n\nimport androidx.lifecycle.LiveData;\nimport androidx.room.Dao;\nimport androidx.room.Delete;\nimport androidx.room.Insert;\nimport androidx.room.OnConflictStrategy;\nimport androidx.room.Query;\nimport androidx.room.Transaction;\nimport androidx.room.Update;\n\nimport java.util.List;\n\n@Dao\npublic interface FeedDao {\n    @Query(\"SELECT * FROM feed WHERE uid = :uid\")\n    List<Feed> getAll(String uid);\n\n    @Query(\"SELECT * FROM feed WHERE uid = :uid\")\n    LiveData<List<Feed>> getAllLiveData(String uid);\n\n\n    @Query(\"SELECT * FROM feed WHERE uid = :uid AND id = :id LIMIT 1\")\n    Feed getById(String uid,String id);\n\n//    @Query(\"SELECT feed.* FROM feed \" +\n//            \"LEFT JOIN feedcategory ON (feed.uid = feedcategory.uid AND feed.id = feedcategory.feedId) \" +\n//            \"WHERE feed.uid = :uid \" +\n//            \"AND feedcategory.categoryId = :categoryId \" +\n//            \"ORDER BY case when feed.unreadCount > 0 then 0 else 1 end, feed.title ASC\")\n\n    @Query(\"SELECT * FROM feed \" +\n            \"WHERE feed.uid = :uid \" +\n            \"AND id IN ( SELECT feedid FROM feedcategory WHERE categoryId = :categoryId) \" +\n            \"ORDER BY CASE WHEN feed.unreadCount > 0 THEN 0 ELSE 1 END, feed.title COLLATE NOCASE ASC\")\n    List<Feed> getByCategoryId(String uid,String categoryId);\n\n    @Query(\"SELECT id,title,unreadCount as count FROM feed \" +\n            \"WHERE feed.uid = :uid \" +\n            \"AND id IN ( SELECT feedid FROM feedcategory WHERE categoryId = :categoryId) \" +\n            \"ORDER BY CASE WHEN feed.unreadCount > 0 THEN 0 ELSE 1 END, feed.title COLLATE NOCASE ASC\")\n    List<Collection> getFeedsUnreadCountByCategoryId(String uid, String categoryId);\n\n    @Query(\"SELECT id,title,starCount as count FROM feed \" +\n            \"WHERE feed.uid = :uid \" +\n            \"AND id IN ( SELECT feedid FROM feedcategory WHERE categoryId = :categoryId) \" +\n            \"ORDER BY CASE WHEN feed.starCount > 0 THEN 0 ELSE 1 END, feed.title COLLATE NOCASE ASC\")\n    List<Collection> getFeedsStarCountByCategoryId(String uid, String categoryId);\n\n    @Query(\"SELECT id,title,allCount as count FROM feed \" +\n            \"WHERE feed.uid = :uid \" +\n            \"AND id IN ( SELECT feedid FROM feedcategory WHERE categoryId = :categoryId) \" +\n            \"ORDER BY CASE WHEN feed.allCount > 0 THEN 0 ELSE 1 END, feed.title COLLATE NOCASE ASC\")\n    List<Collection> getFeedsAllCountByCategoryId(String uid, String categoryId);\n\n\n    @Query(\"SELECT * FROM FeedView WHERE uid = :uid\" )\n    List<Feed> getFeedsRealTimeCount(String uid);\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    @Transaction\n    void insert(Feed... feeds);\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    @Transaction\n    void insert(List<Feed> feeds);\n\n    @Update\n    @Transaction\n    void update(Feed... feeds);\n    @Update\n    @Transaction\n    void update(List<Feed> feeds);\n\n    @Delete\n    @Transaction\n    void delete(Feed... feeds);\n    @Delete\n    @Transaction\n    void delete(List<Feed> feeds);\n\n    @Query(\"DELETE FROM feed WHERE uid = (:uid) AND id = :id\")\n    void deleteById(String uid, String id);\n\n    @Query(\"DELETE FROM feed WHERE uid = (:uid)\")\n    void clear(String... uid);\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/FeedView.java",
    "content": "package me.wizos.loread.db;\n\nimport androidx.room.DatabaseView;\n\nimport me.wizos.loread.App;\n\n/**\n * Feed 与 Category 是 多对多关系，即一个 Feed 可以存在与多个 Category 中，Category 也可以包含多个 Feed\n * Created by Wizos on 2020/3/17.\n */\n@DatabaseView(\n        \"SELECT uid,id,title,feedUrl,htmlUrl,iconUrl,displayMode,UNREAD_SUM AS unreadCount,STAR_SUM AS starCount,ALL_SUM AS allCount,state FROM FEED\" +\n        \" LEFT JOIN (SELECT uid AS article_uid, feedId, COUNT(1) AS UNREAD_SUM FROM article WHERE readStatus != \" + App.STATUS_READED + \" GROUP BY uid,feedId) A ON FEED.uid = A.article_uid AND FEED.id = A.feedId\" +\n        \" LEFT JOIN (SELECT uid AS article_uid, feedId, COUNT(1) AS STAR_SUM FROM article WHERE starStatus = \" + App.STATUS_STARED + \" GROUP BY uid,feedId) B ON FEED.uid = B.article_uid AND FEED.id = B.feedId\" +\n        \" LEFT JOIN (SELECT uid AS article_uid, feedId, COUNT(1) AS ALL_SUM FROM article GROUP BY uid,feedId) C ON FEED.uid = c.article_uid AND FEED.id = C.feedId\"\n)\npublic class FeedView extends Feed{\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/Tag.java",
    "content": "package me.wizos.loread.db;\n\nimport androidx.annotation.NonNull;\nimport androidx.room.Entity;\nimport androidx.room.ForeignKey;\nimport androidx.room.Index;\n\nimport static androidx.room.ForeignKey.CASCADE;\n\n/**\n * Created by Wizos on 2020/5/25.\n */\n\n@Entity(primaryKeys = {\"id\",\"uid\"},\n        indices = {@Index({\"id\"}),@Index({\"uid\"}),@Index({\"title\"})},\n        foreignKeys = @ForeignKey(entity = User.class, parentColumns = \"id\", childColumns = \"uid\", onDelete = CASCADE))\npublic class Tag {\n    @NonNull\n    private String id;\n    @NonNull\n    private String uid;\n    private String title;\n\n    private int unreadCount;\n    private int starCount;\n    private int allCount;\n\n    public String getUid() {\n        return uid;\n    }\n\n    public void setUid(String uid) {\n        this.uid = uid;\n    }\n\n    public String getId() {\n        return this.id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getTitle() {\n        return this.title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    public int getUnreadCount() {\n        return unreadCount;\n    }\n\n    public void setUnreadCount(int unreadCount) {\n        this.unreadCount = unreadCount;\n    }\n\n    public int getStarCount() {\n        return starCount;\n    }\n\n    public void setStarCount(int starCount) {\n        this.starCount = starCount;\n    }\n\n    public int getAllCount() {\n        return allCount;\n    }\n\n    public void setAllCount(int allCount) {\n        this.allCount = allCount;\n    }\n\n    public Category convert(){\n        Category category = new Category();\n        category.setId(id);\n        category.setUid(uid);\n        category.setTitle(title);\n        category.setUnreadCount(unreadCount);\n        category.setStarCount(starCount);\n        category.setAllCount(allCount);\n        return category;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/TagDao.java",
    "content": "package me.wizos.loread.db;\n\nimport androidx.lifecycle.LiveData;\nimport androidx.room.Dao;\nimport androidx.room.Delete;\nimport androidx.room.Insert;\nimport androidx.room.OnConflictStrategy;\nimport androidx.room.Query;\nimport androidx.room.Transaction;\nimport androidx.room.Update;\n\nimport java.util.List;\n\n@Dao\npublic interface TagDao {\n    @Query(\"SELECT * FROM tag WHERE uid = :uid ORDER BY title COLLATE NOCASE ASC\")\n    List<Tag> getAll(String uid);\n\n    @Query(\"SELECT * FROM tag WHERE uid = :uid ORDER BY title COLLATE NOCASE ASC\")\n    LiveData<List<Tag>> getAllLiveData(String uid);\n\n\n    @Query(\"SELECT * FROM tag WHERE uid = :uid AND id = :id LIMIT 1\")\n    Tag getById(String uid, String id);\n\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    @Transaction\n    void insert(Tag... tags);\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    @Transaction\n    void insert(List<Tag> tags);\n\n    @Update\n    @Transaction\n    void update(Tag... tags);\n\n    @Update\n    @Transaction\n    void update(List<Tag> tags);\n\n    @Delete\n    @Transaction\n    void delete(Tag... tags);\n\n    @Query(\"DELETE FROM tag WHERE uid = (:uid)\")\n    void clear(String... uid);\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/User.java",
    "content": "package me.wizos.loread.db;\n\nimport androidx.annotation.NonNull;\nimport androidx.room.Entity;\nimport androidx.room.Index;\nimport androidx.room.PrimaryKey;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.bean.Token;\n\n\n/**\n * Created by Wizos on 2020/3/17.\n */\n@Entity(\n        indices = {@Index({\"id\"}),@Index({\"source\"}),@Index({\"userId\"})} )\npublic class User {\n    @NonNull\n    @PrimaryKey\n    private String id;\n    // 账户信息\n    private String source;\n    private String userId; // 该用户在服务商那的id\n    private String userName; // 该用户在服务商那的name\n    private String userEmail;\n    private String userPassword;\n\n    private String tokenType;\n    private String accessToken;\n    private String refreshToken;\n    private String auth;\n    private long expiresTimestamp = 0;\n\n    // 上次操作的状态\n    private String streamId = \"user/\" + userId + App.CATEGORY_ALL;\n    private String streamTitle = App.i().getString(R.string.all);\n\n    private int streamType = App.TYPE_GROUP;\n    private int streamStatus = App.STATUS_ALL;\n\n    // 个人设置偏好\n    private boolean autoSync = true;\n\n    // 自动同步的时间间隔，单位为分钟\n    private int autoSyncFrequency = 30;\n    private boolean autoSyncOnlyWifi = false;\n\n    private boolean downloadImgOnlyWifi = false;\n    private boolean openLinkBySysBrowser = false;\n    //是否滚动标记为已读\n    private boolean markReadOnScroll = false;\n    // 缓存的天数\n    private int cachePeriod = 7;\n    private boolean autoToggleTheme = true;\n    private int themeMode = App.THEME_DAY;\n    private float audioSpeed = 1.0f;\n    private String host;\n\n    public void setToken(Token token) {\n        if (null == token) {\n            return;\n        }\n        accessToken = token.getAccess_token();\n        refreshToken = token.getRefresh_token();\n        tokenType = token.getToken_type();\n        expiresTimestamp = token.getExpires_in() + (System.currentTimeMillis() / 1000);\n        auth = token.getAuth();\n    }\n\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getSource() {\n        return source;\n    }\n\n    public void setSource(String source) {\n        this.source = source;\n    }\n\n    public String getUserId() {\n        return userId;\n    }\n\n    public void setUserId(String userId) {\n        this.userId = userId;\n    }\n\n    public String getUserEmail() {\n        return userEmail;\n    }\n\n    public void setUserEmail(String userEmail) {\n        this.userEmail = userEmail;\n    }\n\n    public String getUserPassword() {\n        return userPassword;\n    }\n\n    public void setUserPassword(String userPassword) {\n        this.userPassword = userPassword;\n    }\n\n    public String getUserName() {\n        return userName;\n    }\n\n    public void setUserName(String userName) {\n        this.userName = userName;\n    }\n\n    public String getTokenType() {\n        return tokenType;\n    }\n\n    public void setTokenType(String tokenType) {\n        this.tokenType = tokenType;\n    }\n\n    public String getAccessToken() {\n        return accessToken;\n    }\n\n    public void setAccessToken(String accessToken) {\n        this.accessToken = accessToken;\n    }\n\n    public String getRefreshToken() {\n        return refreshToken;\n    }\n\n    public void setRefreshToken(String refreshToken) {\n        this.refreshToken = refreshToken;\n    }\n\n    public String getAuth() {\n        return auth;\n    }\n\n    public void setAuth(String auth) {\n        this.auth = auth;\n    }\n\n    public long getExpiresTimestamp() {\n        return expiresTimestamp;\n    }\n\n    public void setExpiresTimestamp(long expiresTimestamp) {\n        this.expiresTimestamp = expiresTimestamp;\n    }\n\n    public int getStreamType(){\n        return streamType;\n    }\n    public void setStreamType(int streamType) {\n        this.streamType = streamType;\n    }\n\n    public String getStreamId() {\n        return streamId;\n    }\n    public void setStreamId(String streamId) {\n        this.streamId = streamId;\n    }\n\n    public String getStreamTitle() {\n        return streamTitle;\n    }\n\n    public void setStreamTitle(String streamTitle) {\n        this.streamTitle = streamTitle;\n    }\n\n    public int getStreamStatus() {\n        return streamStatus;\n    }\n\n    public void setStreamStatus(int streamStatus) {\n        this.streamStatus = streamStatus;\n    }\n\n    public boolean isAutoSync() {\n        return autoSync;\n    }\n\n    public void setAutoSync(boolean autoSync) {\n        this.autoSync = autoSync;\n    }\n\n    public int getAutoSyncFrequency() {\n        return autoSyncFrequency;\n    }\n\n    /**\n     * @param autoSyncFrequency 分钟\n     */\n    public void setAutoSyncFrequency(int autoSyncFrequency) {\n        this.autoSyncFrequency = autoSyncFrequency;\n    }\n\n    public boolean isAutoSyncOnlyWifi() {\n        return autoSyncOnlyWifi;\n    }\n\n    public void setAutoSyncOnlyWifi(boolean autoSyncOnlyWifi) {\n        this.autoSyncOnlyWifi = autoSyncOnlyWifi;\n    }\n\n    public boolean isDownloadImgOnlyWifi() {\n        return downloadImgOnlyWifi;\n    }\n\n    public void setDownloadImgOnlyWifi(boolean downloadImgOnlyWifi) {\n        this.downloadImgOnlyWifi = downloadImgOnlyWifi;\n    }\n\n    public boolean isOpenLinkBySysBrowser() {\n        return openLinkBySysBrowser;\n    }\n\n    public void setOpenLinkBySysBrowser(boolean openLinkBySysBrowser) {\n        this.openLinkBySysBrowser = openLinkBySysBrowser;\n    }\n\n    public boolean isMarkReadOnScroll() {\n        return markReadOnScroll;\n    }\n\n    public void setMarkReadOnScroll(boolean markReadOnScroll) {\n        this.markReadOnScroll = markReadOnScroll;\n    }\n\n    public int getCachePeriod() {\n        return cachePeriod;\n    }\n\n    public void setCachePeriod(int cachePeriod) {\n        this.cachePeriod = cachePeriod;\n    }\n\n    public boolean isAutoToggleTheme() {\n        return autoToggleTheme;\n    }\n\n    public void setAutoToggleTheme(boolean autoToggleTheme) {\n        this.autoToggleTheme = autoToggleTheme;\n    }\n\n    public int getThemeMode() {\n        return themeMode;\n    }\n\n    public void setThemeMode(int themeMode) {\n        this.themeMode = themeMode;\n    }\n\n    public float getAudioSpeed() {\n        return audioSpeed;\n    }\n\n    public void setAudioSpeed(float audioSpeed) {\n        this.audioSpeed = audioSpeed;\n    }\n\n    public String getHost() {\n        return host;\n    }\n\n    public void setHost(String host) {\n        this.host = host;\n    }\n\n    @Override\n    public String toString() {\n        return \"User{\" +\n                \"id='\" + id + '\\'' +\n                \", source='\" + source + '\\'' +\n                \", userId='\" + userId + '\\'' +\n                \", userName='\" + userName + '\\'' +\n                \", userEmail='\" + userEmail + '\\'' +\n                \", userPassword='\" + userPassword + '\\'' +\n                \", tokenType='\" + tokenType + '\\'' +\n                \", accessToken='\" + accessToken + '\\'' +\n                \", refreshToken='\" + refreshToken + '\\'' +\n                \", auth='\" + auth + '\\'' +\n                \", expiresTimestamp=\" + expiresTimestamp +\n                \", streamId='\" + streamId + '\\'' +\n                \", streamTitle='\" + streamTitle + '\\'' +\n                \", streamStatus=\" + streamStatus +\n                \", autoSync=\" + autoSync +\n                \", autoSyncFrequency=\" + autoSyncFrequency +\n                \", autoSyncOnlyWifi=\" + autoSyncOnlyWifi +\n                \", downloadImgOnlyWifi=\" + downloadImgOnlyWifi +\n                \", openLinkBySysBrowser=\" + openLinkBySysBrowser +\n                \", markReadOnScroll=\" + markReadOnScroll +\n                \", cachePeriod=\" + cachePeriod +\n                \", autoToggleTheme=\" + autoToggleTheme +\n                \", themeMode=\" + themeMode +\n                \", audioSpeed=\" + audioSpeed +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/db/UserDao.java",
    "content": "package me.wizos.loread.db;\n\nimport androidx.room.Dao;\nimport androidx.room.Delete;\nimport androidx.room.Insert;\nimport androidx.room.OnConflictStrategy;\nimport androidx.room.Query;\nimport androidx.room.Transaction;\nimport androidx.room.Update;\n\nimport java.util.List;\n\n@Dao\npublic interface UserDao {\n    @Query(\"SELECT * FROM user\")\n    List<User> loadAll();\n\n    @Query(\"SELECT * FROM user WHERE id = :uid  LIMIT 1\")\n    User getById(String uid);\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    @Transaction\n    void insert(User... users);\n\n    @Update\n    @Transaction\n    void update(User... Users);\n\n    @Delete\n    @Transaction\n    void delete(User... users);\n\n    @Query(\"DELETE FROM user WHERE id = (:uid)\")\n    @Transaction\n    void delete(String... uid);\n\n    @Query(\"DELETE FROM user\")\n    @Transaction\n    void clear();\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/extractor/Extractor.java",
    "content": "/*\n * Copyright (C) 2015 hu\n *\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; either version 2\n * of the License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.\n */\npackage me.wizos.loread.extractor;\n\nimport com.orhanobut.logger.Logger;\nimport com.socks.library.KLog;\n\nimport org.jsoup.nodes.Document;\nimport org.jsoup.nodes.Element;\nimport org.jsoup.nodes.Node;\nimport org.jsoup.nodes.TextNode;\nimport org.jsoup.select.Elements;\nimport org.jsoup.select.NodeVisitor;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Extractor could extract content,title,time from news webpage\n *\n * 本工具在是在 WebCollector 的基础上参考 GeneralNewsExtractor 项目，增加“去除干扰元素”的功能，以增加提取精准度\n * @author hu，https://github.com/CrawlScript/WebCollector\n */\npublic class Extractor {\n    private Document doc;\n    public Extractor(Document doc) {\n        this.doc = doc;\n    }\n\n    private HashMap<Element, CountInfo> bodyInfoMap = new HashMap<Element, CountInfo>();\n\n    static class CountInfo {\n        int textCount = 0;\n        int linkTextCount = 0;\n        int tagCount = 0;\n        int linkTagCount = 0;\n        double density = 0;\n        double densitySum = 0;\n        //double score = 0;\n        int pCount = 0;\n        int punctuationCount = 0;\n        //double sbdi = 0;\n        ArrayList<Integer> leafList = new ArrayList<Integer>();\n    }\n\n    // ,iframe ,br\n    private void cleanBodyElement(Element body) {\n        body.select(\"script, noscript, style, link\").remove();\n        //body.select(\"*.share, *.contribution, *.copyright, *.copy-right, *.disclaimer, *.recommend, *.related, *.footer, *.comment, *.social, *.submeta\").remove();\n        body.select(\"[id*=share], [id*=contribution], [id*=copyright], [id*=copy-right], [id*=-nav], [id*=-tags], [id*=disclaimer], [id*=recommend], [id*=related], [id*=relates], [id*=archive], [id*=recent_comments], [id*=footer], [id*=social], [id*=submeta], [id*=entry-meta]\").remove();\n        body.select(\"[class*=share], [class*=contribution], [class*=copyright], [class*=copy-right], [class*=-nav], [class*=-tags], [class*=disclaimer], [class*=recommend], [class*=related], [class*=relates], [class*=archive], [class*=recent_comments], [class*=footer], [class*=social], [class*=submeta], [class*=entry-meta]\").remove();\n\n//        // 以下为新增，主要是去除文章中的干扰元素\n//        body.select(\"p:empty, div:empty, blockquote:empty, details:empty, details:empty, figure:empty, figcaption:empty\").remove();\n//        body.select(\"ul:empty, ol:empty, li:empty\").remove();\n//        body.select(\"table:empty, tbody:empty, th:empty, tr:empty, dt:empty, dl:empty\").remove();\n//        // 受 readability 启发，移除没有子元素（包括文本）的元素。\n//        body.select(\"section:empty, h1:empty, h2:empty, h3:empty, h4:empty, h5:empty, h6:empty, ins:empty, a:empty, b:empty, string:empty, span:empty, i:empty\").remove();\n\n        Elements elements;\n        boolean circulate;\n        do {\n            elements = body.select(\"p:empty, div:empty, blockquote:empty, details:empty, details:empty, figure:empty, figcaption:empty,  ul:empty, ol:empty, li:empty,  table:empty, tbody:empty, th:empty, tr:empty, dt:empty, dl:empty,  section:empty, h1:empty, h2:empty, h3:empty, h4:empty, h5:empty, h6:empty, ins:empty, a:empty, b:empty, string:empty, span:empty, i:empty,  section:empty, h1:empty, h2:empty, h3:empty, h4:empty, h5:empty, h6:empty, ins:empty, a:empty, b:empty, string:empty, span:empty, i:empty\");\n            if( elements != null && elements.size() > 0){\n                //System.out.println(\"继续移除：\" + elements.outerHtml());\n                elements.remove();\n                circulate = true;\n            }else {\n                circulate = false;\n            }\n        }while (circulate);\n    }\n\n\n    private CountInfo computeInfo(Node node) {\n        if (node instanceof Element) {\n            Element tag = (Element) node;\n            CountInfo countInfo = new CountInfo();\n            for (Node childNode : tag.childNodes()) {\n                CountInfo childCountInfo = computeInfo(childNode);\n                countInfo.textCount += childCountInfo.textCount; // 子节点的字符串字数\n                countInfo.punctuationCount += childCountInfo.punctuationCount; // 子节点的标点符号的数量\n                countInfo.linkTextCount += childCountInfo.linkTextCount;\n                countInfo.tagCount += childCountInfo.tagCount;\n                countInfo.linkTagCount += childCountInfo.linkTagCount;\n                countInfo.leafList.addAll(childCountInfo.leafList);\n                countInfo.densitySum += childCountInfo.density;\n                countInfo.pCount += childCountInfo.pCount;\n            }\n\n            String tagName = tag.tagName();\n            if (tagName.equals(\"a\")) {\n                countInfo.linkTextCount = countInfo.textCount;\n                countInfo.linkTagCount++;\n            } else if (tagName.equals(\"p\") || tagName.equals(\"article\")) { // || tagName.equals(\"img\") || tagName.equals(\"video\") || tagName.equals(\"audio\")\n                countInfo.pCount++;\n            }\n            // 一些有样式意义的空元素不要计入打分规则，不然会有很严重的干扰，比如91论坛的文章\n            else if(tagName.equals(\"br\") || tagName.equals(\"hr\")){ //  || tagName.equals(\"strong\") || tagName.equals(\"span\")\n                return countInfo;\n            }\n\n            countInfo.tagCount++;\n\n            int pureLen = countInfo.textCount - countInfo.linkTextCount;\n            int len = countInfo.tagCount - countInfo.linkTagCount;\n            if (pureLen == 0 || len == 0) {\n                countInfo.density = 0;\n            } else {\n                countInfo.density = (pureLen + 0.0) / len;\n            }\n\n\n//            increase_tag_weight(tag, countInfo);\n//            countInfo.sbdi = calcSbdi(countInfo);\n//            countInfo.sbdi = (countInfo.textCount - countInfo.linkTextCount + 0.0)/(countInfo.punctuationCount + 1);\n//            // sbdi 不能为0，否则会导致求对数时报错。\n//            if(countInfo.sbdi == 0){\n//                countInfo.sbdi = 1;\n//            }\n\n//            if( tag.className().equals(\"postmessage_5451354\")){\n//                System.out.println(\"节点: \"  + tag.nodeName() + \".\" + tag.className()\n//                                + \", 文本长度：\" + countInfo.textCount + \", 链接文本长度：\" + countInfo.linkTextCount + \", tag数量\" + countInfo.tagCount + \", linkTag：\" + countInfo.linkTagCount\n//                                + \", p标签：\" + countInfo.pCount+ \" , 文字密度：\" + countInfo.density + \",文本密度总数：\" + countInfo.densitySum + \", 符号密度：\"\n//                          );\n//            }\n\n            bodyInfoMap.put(tag, countInfo);\n\n            return countInfo;\n        } else if (node instanceof TextNode) {\n            TextNode tn = (TextNode) node;\n            CountInfo countInfo = new CountInfo();\n            String text = tn.text();\n            int len = text.length();\n            countInfo.textCount = len;\n            //countInfo.punctuationCount = countPunctuationNum(text);\n            countInfo.leafList.add(len);\n            return countInfo;\n        } else {\n            return new CountInfo();\n        }\n    }\n\n    private double computeScore(Element tag) {\n        CountInfo countInfo = bodyInfoMap.get(tag);\n        double var = Math.sqrt(computeVar(countInfo.leafList) + 1); // 这一传是干嘛用的？\n        //  * Math.log(var) * Math.log(countInfo.textCount - countInfo.linkTextCount + 1)\n        return countInfo.densitySum * Math.log10(countInfo.pCount + 2) * Math.log(var) * Math.log(countInfo.textCount - countInfo.linkTextCount + 1);// 最后2个中有一个是论文中计算节点文本的标准差？根据另一个库的建议，不再需要计算文本密度的标准差\n//        return countInfo.densitySum * Math.log10(countInfo.pCount + 2) * Math.log(countInfo.sbdi);\n    }\n\n    /**\n     * 计算某个节点的符号密度\n     */\n    private double calcSbdi(CountInfo countInfo){\n        double sbdi = (countInfo.textCount - countInfo.linkTextCount + 0.0)/(countInfo.punctuationCount + 1);\n        // sbdi 不能为0，否则会导致求对数时报错。\n        if(sbdi == 0){\n            return 1;\n        }else {\n            return sbdi;\n        }\n    }\n    private int countPunctuationNum(String text){\n        String regEx=\"[^`~!@#$%^&*()—\\\\-+=|{}':;,\\\\[\\\\].<>/?！￥…（）《》｛｝【】‘；：”“’。， 、？]\";\n        Pattern p = Pattern.compile(regEx);\n        Matcher m = p.matcher(text);//这里把想要替换的字符串传进来\n        return m.replaceAll(\"\").trim().length();\n    }\n    private void increase_tag_weight(Element element, CountInfo countInfo){\n        if(element.hasClass(\"content\") || element.hasClass(\"article\") || element.hasClass(\"post\") || element.hasClass(\"news\")){\n            countInfo.textCount = countInfo.textCount *2;\n        }\n    }\n\n    // 用这种方式而不是符号密度，貌似速度更快\n    private double computeVar(ArrayList<Integer> data) {\n        if (data.size() == 0) {\n            return 0;\n        }\n        if (data.size() == 1) {\n            return (double)data.get(0) / 2;\n        }\n        double sum = 0;\n        for (Integer i : data) {\n            sum += i;\n        }\n        double ave = sum / data.size();\n        sum = 0;\n        for (Integer i : data) {\n            sum += (i - ave) * (i - ave);\n        }\n        sum = sum / data.size();\n        return sum;\n    }\n\n    /**\n     * 我自己改的，去掉了报错\n     *\n     * @return\n     */\n    public Element getContentElement() {\n        Element bodyElement = doc.body();\n        cleanBodyElement(bodyElement);\n        computeInfo(bodyElement);\n        double maxScore = 0;\n        Element content = null;\n//        String tt = \"\";\n//        flag = flag.trim();\n        for (Map.Entry<Element, CountInfo> entry : bodyInfoMap.entrySet()) {\n            Element tag = entry.getKey();\n            if (tag.tagName().equals(\"a\") || tag == bodyElement) {\n                continue;\n            }\n            double score = computeScore(tag);\n\n//            tt = tag.text().trim();\n//            if( tt.length() > 18){\n//                tt = tt.substring(0,18);\n//            }\n//            if(tt.startsWith(flag)){\n//                score = score * 1.5;\n//            }\n//            System.out.println(\"Tag: \"  + tag.nodeName() + \".\" + tag.className()  + \" , 分数：\" + score + \" , 文本：\" + tt);\n            if (score > maxScore) {\n                maxScore = score;\n                content = tag;\n            }\n        }\n\n        if (content != null) {\n//            KLog.e(\"正文是：\" + content.text());\n            return content;\n        }\n        Logger.e(\"提取失败\");\n        return null;\n    }\n\n    public ModPage getNews() throws Exception {\n        ModPage modPage = new ModPage();\n        Element contentElement;\n        try {\n            contentElement = getContentElement();\n            modPage.setContentElement(contentElement);\n        } catch (Exception ex) {\n            KLog.e(\"modPage content extraction failed,extraction abort\", ex);\n            throw new Exception(ex);\n        }\n\n        if (doc.baseUri() != null) {\n            modPage.setUrl(doc.baseUri());\n        }\n\n        try {\n            modPage.setTime(getTime(contentElement));\n        } catch (Exception ex) {\n            KLog.e(\"modPage title extraction failed\", ex);\n        }\n\n        try {\n            modPage.setTitle(getTitle(contentElement));\n        } catch (Exception ex) {\n            KLog.e(\"title extraction failed\", ex);\n        }\n        return modPage;\n    }\n\n    private String getTime(Element contentElement) throws Exception {\n        String regex = \"([1-2][0-9]{3})[^0-9]{1,5}?([0-1]?[0-9])[^0-9]{1,5}?([0-9]{1,2})[^0-9]{1,5}?([0-2]?[1-9])[^0-9]{1,5}?([0-9]{1,2})[^0-9]{1,5}?([0-9]{1,2})\";\n        Pattern pattern = Pattern.compile(regex);\n        Element current = contentElement;\n        for (int i = 0; i < 2; i++) {\n            if (current != null && current != doc.body()) {\n                Element parent = current.parent();\n                if (parent != null) {\n                    current = parent;\n                }\n            }\n        }\n        for (int i = 0; i < 6; i++) {\n            if (current == null) {\n                break;\n            }\n            String currentHtml = current.outerHtml();\n            Matcher matcher = pattern.matcher(currentHtml);\n            if (matcher.find()) {\n                return matcher.group(1) + \"-\" + matcher.group(2) + \"-\" + matcher.group(3) + \" \" + matcher.group(4) + \":\" + matcher.group(5) + \":\" + matcher.group(6);\n            }\n            if (current != doc.body()) {\n                current = current.parent();\n            }\n        }\n\n        try {\n            return getDate(contentElement);\n        } catch (Exception ex) {\n            throw new Exception(\"time not found\");\n        }\n\n    }\n\n    private String getDate(Element contentElement) throws Exception {\n        String regex = \"([1-2][0-9]{3})[^0-9]{1,5}?([0-1]?[0-9])[^0-9]{1,5}?([0-9]{1,2})\";\n        Pattern pattern = Pattern.compile(regex);\n        Element current = contentElement;\n        for (int i = 0; i < 2; i++) {\n            if (current != null && current != doc.body()) {\n                Element parent = current.parent();\n                if (parent != null) {\n                    current = parent;\n                }\n            }\n        }\n        for (int i = 0; i < 6; i++) {\n            if (current == null) {\n                break;\n            }\n            String currentHtml = current.outerHtml();\n            Matcher matcher = pattern.matcher(currentHtml);\n            if (matcher.find()) {\n                return matcher.group(1) + \"-\" + matcher.group(2) + \"-\" + matcher.group(3);\n            }\n            if (current != doc.body()) {\n                current = current.parent();\n            }\n        }\n        throw new Exception(\"date not found\");\n    }\n\n    private double strSim(String a, String b) {\n        int len1 = a.length();\n        int len2 = b.length();\n        if (len1 == 0 || len2 == 0) {\n            return 0;\n        }\n        double ratio;\n        if (len1 > len2) {\n            ratio = (len1 + 0.0) / len2;\n        } else {\n            ratio = (len2 + 0.0) / len1;\n        }\n        if (ratio >= 3) {\n            return 0;\n        }\n        return (lcs(a, b) + 0.0) / Math.max(len1, len2);\n    }\n\n    private String getTitle(final Element contentElement) throws Exception {\n        final ArrayList<Element> titleList = new ArrayList<Element>();\n        final ArrayList<Double> titleSim = new ArrayList<Double>();\n        final AtomicInteger contentIndex = new AtomicInteger();\n        final String metaTitle = doc.title().trim();\n        if (!metaTitle.isEmpty()) {\n            doc.body().traverse(new NodeVisitor() {\n                @Override\n                public void head(Node node, int i) {\n                    if (node instanceof Element) {\n                        Element tag = (Element) node;\n                        if (tag == contentElement) {\n                            contentIndex.set(titleList.size());\n                            return;\n                        }\n                        String tagName = tag.tagName();\n                        if (Pattern.matches(\"h[1-6]\", tagName)) {\n                            String title = tag.text().trim();\n                            double sim = strSim(title, metaTitle);\n                            titleSim.add(sim);\n                            titleList.add(tag);\n                        }\n                    }\n                }\n\n                @Override\n                public void tail(Node node, int i) {\n                }\n            });\n            int index = contentIndex.get();\n            if (index > 0) {\n                double maxScore = 0;\n                int maxIndex = -1;\n                for (int i = 0; i < index; i++) {\n                    double score = (i + 1) * titleSim.get(i);\n                    if (score > maxScore) {\n                        maxScore = score;\n                        maxIndex = i;\n                    }\n                }\n                if (maxIndex != -1) {\n                    return titleList.get(maxIndex).text();\n                }\n            }\n        }\n\n        Elements titles = doc.body().select(\"*[id^=title],*[id$=title],*[class^=title],*[class$=title]\");\n        if (titles.size() > 0) {\n            String title = titles.first().text();\n            if (title.length() > 5 && title.length() < 40) {\n                return titles.first().text();\n            }\n        }\n        try {\n            return getTitleByEditDistance(contentElement);\n        } catch (Exception ex) {\n            throw new Exception(\"title not found\");\n        }\n\n    }\n\n    private String getTitleByEditDistance(Element contentElement) throws Exception {\n        final String metaTitle = doc.title();\n\n        final ArrayList<Double> max = new ArrayList<Double>();\n        max.add(0.0);\n        final StringBuilder sb = new StringBuilder();\n        doc.body().traverse(new NodeVisitor() {\n            @Override\n            public void head(Node node, int i) {\n                if (node instanceof TextNode) {\n                    TextNode tn = (TextNode) node;\n                    String text = tn.text().trim();\n                    double sim = strSim(text, metaTitle);\n                    if (sim > 0) {\n                        if (sim > max.get(0)) {\n                            max.set(0, sim);\n                            sb.setLength(0);\n                            sb.append(text);\n                        }\n                    }\n\n                }\n            }\n\n            @Override\n            public void tail(Node node, int i) {\n            }\n        });\n        if (sb.length() > 0) {\n            return sb.toString();\n        }\n        throw new Exception();\n\n    }\n\n    private int lcs(String x, String y) {\n        int M = x.length();\n        int N = y.length();\n        if (M == 0 || N == 0) {\n            return 0;\n        }\n        int[][] opt = new int[M + 1][N + 1];\n\n        for (int i = M - 1; i >= 0; i--) {\n            for (int j = N - 1; j >= 0; j--) {\n                if (x.charAt(i) == y.charAt(j)) {\n                    opt[i][j] = opt[i + 1][j + 1] + 1;\n                } else {\n                    opt[i][j] = Math.max(opt[i + 1][j], opt[i][j + 1]);\n                }\n            }\n        }\n\n        return opt[0][0];\n\n    }\n\n    private int editDistance(String word1, String word2) {\n        int len1 = word1.length();\n        int len2 = word2.length();\n\n        int[][] dp = new int[len1 + 1][len2 + 1];\n\n        for (int i = 0; i <= len1; i++) {\n            dp[i][0] = i;\n        }\n\n        for (int j = 0; j <= len2; j++) {\n            dp[0][j] = j;\n        }\n\n        for (int i = 0; i < len1; i++) {\n            char c1 = word1.charAt(i);\n            for (int j = 0; j < len2; j++) {\n                char c2 = word2.charAt(j);\n\n                if (c1 == c2) {\n                    dp[i + 1][j + 1] = dp[i][j];\n                } else {\n                    int replace = dp[i][j] + 1;\n                    int insert = dp[i][j + 1] + 1;\n                    int delete = dp[i + 1][j] + 1;\n\n                    int min = replace > insert ? insert : replace;\n                    min = delete > min ? min : delete;\n                    dp[i + 1][j + 1] = min;\n                }\n            }\n        }\n\n        return dp[len1][len2];\n    }\n\n\n//    /*输入Jsoup的Document，获取正文文本*/\n//    public static String getContentByDoc(Document doc,String mKeyWord) { // throws Exception\n//        Extractor ce = new Extractor(doc,mKeyWord);\n//        Element newDoc = ce.getContentElement();\n//\n//        KLog.e(\"含关键字的正文3是：\" + newDoc.outerHtml());\n//        if( newDoc == null ){\n//            return \"\";\n//        }\n//        KLog.e(\"含关键字的正文4是：\" + newDoc.outerHtml());\n//        return newDoc.outerHtml();\n//    }\n\n//    /*输入HTML，获取正文文本*/\n//    public static String getContentByHtml(String html) throws Exception {\n//        Document doc = Jsoup.parse(html);\n//        return getContentElementByDoc(doc).text();\n//    }\n//    /*输入HTML，获取正文文本*/\n//    public static String getContentHtml(String html) throws Exception {\n//        Document doc = Jsoup.parse(html);\n//        return getContentElementByDoc(doc).text();\n//    }\n//    public static String getContentHtml(String baseUri,InputStream inputStream) throws Exception {\n//        Document doc = Jsoup.parse(inputStream,null,baseUri);\n//        KLog.e(\"编码是2：\" + doc.charset() );\n//        return getContentElementByDoc(doc).outerHtml();\n//    }\n//    public static String getContentHtml(String baseUri,Document doc) throws Exception {\n//        KLog.e(\"编码是2：\" + doc.charset() );\n//        return getContentElementByDoc(doc).outerHtml();\n//    }\n//    public static String getContentHtmlByUrl(String content) throws Exception {\n//        Document doc = Jsoup.connect(content).get();\n//        return getContentElementByDoc(doc).outerHtml();\n//    }\n\n//    /*输入HTML和URL，获取正文文本*/\n//    public static String getContentByHtml(String html, String content) throws Exception {\n//        Document doc = Jsoup.parse(html, content);\n//        return getContentElementByDoc(doc).text();\n//    }\n\n//    /*输入URL，获取正文文本*/\n//    public static String getContentByUrl(String content) throws Exception {\n//        HttpRequest request = new HttpRequest(content);\n//        String html = request.response().decode();\n//        return getContentByHtml(html, content);\n//    }\n\n//    /*输入Jsoup的Document，获取结构化新闻信息*/\n//    public static ModPage getNewsByDoc(Document doc) throws Exception {\n//        Extractor ce = new Extractor(doc);\n//        return ce.getNews();\n//    }\n\n//    /*输入HTML，获取结构化新闻信息*/\n//    public static ModPage getNewsByHtml(String html) throws Exception {\n//        Document doc = Jsoup.parse(html);\n//        return getNewsByDoc(doc);\n//    }\n\n//    /*输入HTML和URL，获取结构化新闻信息*/\n//    public static ModPage getNewsByHtml(String html, String content) throws Exception {\n//        Document doc = Jsoup.parse(html, content);\n//        return getNewsByDoc(doc);\n//    }\n\n//    /*输入URL，获取结构化新闻信息*/\n//    public static ModPage getNewsByUrl(String content) throws Exception {\n////        HttpRequest request = new HttpRequest(content);\n////        String html = request.response().decode();\n//        return getNewsByHtml(OkGo.get(content).execute().body().string(), content);\n//    }\n\n//    public static void main(String[] args) throws Exception {\n//        ModPage news = Extractor.getNewsByUrl(\"http://www.huxiu.com/article/121959/1.html\");\n//        System.out.println(news.getContent());\n//        System.out.println(news.getTitle());\n//        System.out.println(news.getTime());\n//        System.out.println(news.getData());\n//        //System.out.println(news.getContentElement());\n//        //System.out.println(news);\n//    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/extractor/ExtractorUtil.java",
    "content": "package me.wizos.loread.extractor;\n\nimport android.net.Uri;\n\nimport com.hjq.toast.ToastUtils;\nimport com.socks.library.KLog;\n\nimport org.jsoup.nodes.Document;\nimport org.jsoup.nodes.Element;\nimport org.jsoup.select.Elements;\n\nimport javax.script.Bindings;\nimport javax.script.SimpleBindings;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.config.ArticleExtractConfig;\nimport me.wizos.loread.config.article_extract_rule.ArticleExtractRule;\nimport me.wizos.loread.utils.ScriptUtil;\nimport me.wizos.loread.utils.StringUtils;\n\npublic class ExtractorUtil {\n    /*输入Jsoup的Document，获取正文文本*/\n    public static String getContent(String url, Document doc) { // throws Exception\n        Uri uri = Uri.parse(url);\n        ArticleExtractRule rule;\n\n        String content;\n        rule = ArticleExtractConfig.i().getRuleByDomain(uri.getHost());\n        if(rule != null){\n            content = getContentByRule(uri, doc, rule);\n            if(!StringUtils.isEmpty(content)){\n                return content;\n            }else {\n                KLog.e(\"规则失效A\");\n                ToastUtils.show(App.i().getString(R.string.the_rule_of_full_text_extraction_has_expired, uri.getHost()));\n            }\n        }\n\n        rule = ArticleExtractConfig.i().getRuleByCssSelector(doc);\n        if(rule != null){\n            content = getContentByRule(uri, doc, rule);\n            if(!StringUtils.isEmpty(content)){\n                return content;\n            }else {\n                KLog.e(\"规则失效B\");\n                ToastUtils.show(App.i().getString(R.string.the_rule_of_full_text_extraction_has_expired, uri.getHost()));\n            }\n        }\n\n        //rule = ArticleExtractRuleConfig.i().getRuleByRegex(doc.outerHtml());\n        //if(rule != null){\n        //    return getContentByRule(uri, doc, rule);\n        //}\n\n        return getContentByExtractor(uri.getHost(), doc);\n    }\n\n    private static String getContentByRule(Uri uri, Document doc, ArticleExtractRule rule) {\n        if( !StringUtils.isEmpty(rule.getDocumentTrim()) ){\n            Bindings bindings = new SimpleBindings();\n            bindings.put(\"document\", doc);\n            bindings.put(\"uri\", uri);\n            ScriptUtil.i().eval(rule.getDocumentTrim(), bindings);\n        }\n\n        if( !StringUtils.isEmpty(rule.getContent()) ){\n            KLog.i(\"提取规则\", \"正文：\" + rule.getContent() );\n            Elements contentElements = doc.select(rule.getContent());\n            if (!StringUtils.isEmpty(rule.getContentStrip())) {\n                KLog.i(\"提取规则\", \"正文过滤：\" + rule.getContentStrip() );\n                // 移除不需要的内容，注意规则为空\n                contentElements.select(rule.getContentStrip()).remove();\n            }\n\n            if( !StringUtils.isEmpty(rule.getContentTrim()) ){\n                Bindings bindings = new SimpleBindings();\n                bindings.put(\"content\", contentElements.html());\n                ScriptUtil.i().eval(rule.getContentTrim(), bindings);\n                KLog.i(\"提取规则\", \"正文处理：\" + rule.getContentTrim() );\n                return (String)bindings.get(\"content\");\n            }\n            return contentElements.html().trim();\n        }\n        return null;\n    }\n\n    /*输入Jsoup的Document，获取正文文本*/\n    private static String getContentByExtractor(String domain, Document doc) { // throws Exception\n        Element newDoc = new Extractor(doc).getContentElement();\n        if (newDoc == null) {\n            return \"\";\n        }\n        KLog.i(\"自动获取规则：\" + newDoc.cssSelector());\n        String tmp1 = newDoc.cssSelector();\n        ArticleExtractConfig.i().saveRuleByDomain(doc, domain,newDoc.cssSelector());\n        return newDoc.html();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/extractor/ModPage.java",
    "content": "/*\n * Copyright (C) 2015 hu\n *\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; either version 2\n * of the License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.\n */\npackage me.wizos.loread.extractor;\n\nimport org.jsoup.nodes.Element;\n\n/**\n * @author hu\n */\npublic class ModPage {\n\n    protected String url = null;\n    protected String title = null;\n    protected String content = null;\n    protected String time = null;\n\n    protected Element contentElement = null;\n\n    public String getUrl() {\n        return url;\n    }\n\n    public void setUrl(String url) {\n        this.url = url;\n    }\n\n    public String getTitle() {\n        return title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    public String getContent() {\n        if (content == null) {\n            if (contentElement != null) {\n                content = contentElement.text();\n            }\n        }\n        return content;\n    }\n\n\n    public void setContent(String content) {\n        this.content = content;\n    }\n\n    public String getTime() {\n        return time;\n    }\n\n    public void setTime(String time) {\n        this.time = time;\n    }\n\n    @Override\n    public String toString() {\n        return \"URL:\\n\" + url + \"\\nTITLE:\\n\" + title + \"\\nTIME:\\n\" + time + \"\\nCONTENT:\\n\" + getContent() + \"\\nCONTENT(SOURCE):\\n\" + contentElement;\n    }\n\n    public Element getContentElement() {\n        return contentElement;\n    }\n\n    public void setContentElement(Element contentElement) {\n        this.contentElement = contentElement;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/gson/GsonEnum.java",
    "content": "package me.wizos.loread.gson;\n\npublic interface GsonEnum<E> {\n    String serialize();\n    E deserialize(String jsonEnum);\n}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/gson/GsonEnumTypeAdapter.java",
    "content": "package me.wizos.loread.gson;\n\nimport com.google.gson.JsonDeserializationContext;\nimport com.google.gson.JsonDeserializer;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonParseException;\nimport com.google.gson.JsonPrimitive;\nimport com.google.gson.JsonSerializationContext;\nimport com.google.gson.JsonSerializer;\n\nimport java.lang.reflect.Type;\n\npublic class GsonEnumTypeAdapter<E> implements JsonSerializer<E>, JsonDeserializer<E> {\n\n    private final GsonEnum<E> gsonEnum;\n\n    public GsonEnumTypeAdapter(GsonEnum<E> gsonEnum) {\n        this.gsonEnum = gsonEnum;\n    }\n\n    @Override\n    public JsonElement serialize(E src, Type typeOfSrc, JsonSerializationContext context) {\n        if (null != src && src instanceof GsonEnum) {\n            return new JsonPrimitive(((GsonEnum) src).serialize());\n        }\n        return null;\n    }\n\n    @Override\n    public E deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {\n        if (null != json) {\n            return gsonEnum.deserialize(json.getAsString());\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/gson/GsonUtil.java",
    "content": "package me.wizos.loread.gson;\n\nimport com.google.gson.Gson;\nimport com.google.gson.reflect.TypeToken;\n\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.UnsupportedEncodingException;\n\n/**\n * author  Wizos\n * created 2019/8/3\n */\npublic class GsonUtil {\n    private static Gson gson = new Gson();\n\n    public static <T> T fromJson(InputStream inputStream, TypeToken<T> token) {\n        try {\n            return gson.fromJson(new InputStreamReader(inputStream, \"UTF-8\"), token.getType());\n        } catch (UnsupportedEncodingException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    public static <T> T fromJson(String jsonText, TypeToken<T> token) {\n        try {\n            return gson.fromJson(jsonText, token.getType());\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/HttpClientManager.java",
    "content": "package me.wizos.loread.network;\n\nimport com.lzy.okgo.https.HttpsUtils;\n\nimport java.util.concurrent.TimeUnit;\n\nimport me.wizos.loread.network.interceptor.InoreaderHeaderInterceptor;\nimport me.wizos.loread.network.interceptor.LoggerInterceptor;\nimport me.wizos.loread.network.interceptor.LoreadTokenInterceptor;\nimport me.wizos.loread.network.interceptor.RedirectInterceptor;\nimport me.wizos.loread.network.interceptor.RelyInterceptor;\nimport me.wizos.loread.network.interceptor.TTRSSTokenInterceptor;\nimport me.wizos.loread.network.interceptor.TokenAuthenticator;\nimport okhttp3.OkHttpClient;\n\n/**\n * @author Wizos on 2019/5/12.\n */\n\npublic class HttpClientManager {\n    private HttpClientManager() {\n    }\n\n    private static HttpClientManager instance;\n    private static OkHttpClient simpleOkHttpClient;\n    private static OkHttpClient loreadHttpClient;\n    private static OkHttpClient ttrssHttpClient;\n    private static OkHttpClient inoreaderHttpClient;\n    private static OkHttpClient feedlyHttpClient;\n    private static OkHttpClient imageHttpClient;\n\n    public static HttpClientManager i() {\n        if (instance == null) {\n            synchronized (HttpClientManager.class) {\n                if (instance == null) {\n                    instance = new HttpClientManager();\n\n                    loreadHttpClient = new OkHttpClient.Builder()\n                            .readTimeout(30, TimeUnit.SECONDS)\n                            .writeTimeout(30, TimeUnit.SECONDS)\n                            .connectTimeout(15, TimeUnit.SECONDS)\n                            .sslSocketFactory(HttpsUtils.getSslSocketFactory().sSLSocketFactory, HttpsUtils.getSslSocketFactory().trustManager)\n                            .hostnameVerifier(HttpsUtils.UnSafeHostnameVerifier)\n                            .followRedirects(true)\n                            .followSslRedirects(true)\n                            .addInterceptor(new LoreadTokenInterceptor())\n                            .build();\n                    ttrssHttpClient = new OkHttpClient.Builder()\n                            .readTimeout(30, TimeUnit.SECONDS)\n                            .writeTimeout(30, TimeUnit.SECONDS)\n                            .connectTimeout(15, TimeUnit.SECONDS)\n                            .sslSocketFactory(HttpsUtils.getSslSocketFactory().sSLSocketFactory, HttpsUtils.getSslSocketFactory().trustManager)\n                            .hostnameVerifier(HttpsUtils.UnSafeHostnameVerifier)\n                            .followRedirects(true)\n                            .followSslRedirects(true)\n//                            .authenticator(new TTRSSAuthenticator())\n                            .addInterceptor(new TTRSSTokenInterceptor())\n                            .build();\n\n                    inoreaderHttpClient = new OkHttpClient.Builder()\n                            .readTimeout(30, TimeUnit.SECONDS)\n                            .writeTimeout(30, TimeUnit.SECONDS)\n                            .connectTimeout(15, TimeUnit.SECONDS)\n                            .sslSocketFactory(HttpsUtils.getSslSocketFactory().sSLSocketFactory, HttpsUtils.getSslSocketFactory().trustManager)\n                            .hostnameVerifier(HttpsUtils.UnSafeHostnameVerifier)\n                            .followRedirects(true)\n                            .followSslRedirects(true)\n//                            .addInterceptor(new AuthorizationInterceptor())\n                            .addInterceptor(new InoreaderHeaderInterceptor())\n                            .addInterceptor(new LoggerInterceptor())\n                            .authenticator(new TokenAuthenticator())\n//                            .dns(new HttpDNS())\n                            .build();\n\n\n                    feedlyHttpClient = new OkHttpClient.Builder()\n                            .readTimeout(30, TimeUnit.SECONDS)\n                            .writeTimeout(30, TimeUnit.SECONDS)\n                            .connectTimeout(15, TimeUnit.SECONDS)\n                            .sslSocketFactory(HttpsUtils.getSslSocketFactory().sSLSocketFactory, HttpsUtils.getSslSocketFactory().trustManager)\n                            .hostnameVerifier(HttpsUtils.UnSafeHostnameVerifier)\n                            .followRedirects(true)\n                            .followSslRedirects(true)\n//                            .addInterceptor(new AuthorizationInterceptor())\n                            .addInterceptor(new LoggerInterceptor())\n                            .authenticator(new TokenAuthenticator())\n                            .build();\n\n                    simpleOkHttpClient = new OkHttpClient.Builder()\n                            .readTimeout(30, TimeUnit.SECONDS)\n                            .writeTimeout(30, TimeUnit.SECONDS)\n                            .connectTimeout(15, TimeUnit.SECONDS)\n                            .sslSocketFactory(HttpsUtils.getSslSocketFactory().sSLSocketFactory, HttpsUtils.getSslSocketFactory().trustManager)\n                            .hostnameVerifier(HttpsUtils.UnSafeHostnameVerifier)\n                            .followRedirects(true)\n                            .followSslRedirects(true)\n                            .addInterceptor(new RelyInterceptor())\n                            .build();\n                    imageHttpClient = new OkHttpClient.Builder()\n                            .readTimeout(60, TimeUnit.SECONDS)\n                            .writeTimeout(60, TimeUnit.SECONDS)\n                            .connectTimeout(15, TimeUnit.SECONDS)\n                            .sslSocketFactory(HttpsUtils.getSslSocketFactory().sSLSocketFactory, HttpsUtils.getSslSocketFactory().trustManager)\n                            .hostnameVerifier(HttpsUtils.UnSafeHostnameVerifier)\n                            .followRedirects(true)\n                            .followSslRedirects(true)\n                            .addInterceptor(new RedirectInterceptor())\n//                            .addInterceptor(new RefererInterceptor())\n                            .build();\n                    imageHttpClient.dispatcher().setMaxRequests(4);\n                }\n            }\n        }\n        return instance;\n    }\n\n\n    public OkHttpClient simpleClient() {\n        return simpleOkHttpClient;\n    }\n\n    public OkHttpClient loreadHttpClient() {\n        return loreadHttpClient;\n    }\n\n    public OkHttpClient ttrssHttpClient() {return ttrssHttpClient;}\n\n    public OkHttpClient inoreaderHttpClient() {\n        return inoreaderHttpClient;\n    }\n\n    public OkHttpClient feedlyHttpClient() {\n        return feedlyHttpClient;\n    }\n\n    public OkHttpClient imageHttpClient() {\n        return imageHttpClient;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/StringConverterFactory.java",
    "content": "package me.wizos.loread.network;\n\nimport java.io.IOException;\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Type;\n\nimport okhttp3.ResponseBody;\nimport retrofit2.Converter;\nimport retrofit2.Retrofit;\n\npublic class StringConverterFactory extends Converter.Factory {\n    //工厂方法，用于创建实例\n    public static StringConverterFactory create() {\n        return new StringConverterFactory();\n    }\n\n    //response返回到本地后会被调用，这里先判断是否要拦截处理，不拦截则返回null\n    // 判断是否处理的依据就是type参数，type就是上面接口出现的List了\n    @Override\n    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {\n        // KLog.e(\"响应的类型：\" + type );\n        if (type == String.class) {\n            return new StringBodyConverter<Type>();\n        }\n        //如果返回null则不处理，交给别的Converter处理\n        return null;\n    }\n\n    // 一个Converter类，T就是上面接口中的List了\n    private static class StringBodyConverter<T> implements Converter<ResponseBody, T> {\n        StringBodyConverter() {}\n        //在这个方法中处理response\n        @Override\n        public T convert(ResponseBody value) throws IOException {\n            return (T) value.string();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/SyncWorker.java",
    "content": "package me.wizos.loread.network;\n\nimport android.content.Context;\n\nimport androidx.annotation.NonNull;\nimport androidx.work.Worker;\nimport androidx.work.WorkerParameters;\n\nimport com.jeremyliao.liveeventbus.LiveEventBus;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.utils.NetworkUtil;\n\npublic class SyncWorker extends Worker  {\n    public final static String TAG = \"SyncWorker\";\n    public final static String SYNC_TASK_STATUS = \"SyncStatus\";\n    public final static String SYNC_PROCESS_FOR_SUBTITLE = \"SyncProcess\";\n    public final static String NEW_ARTICLE_NUMBER = \"NewArticleNumber\";\n    public SyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {\n        super(context, workerParams);\n    }\n\n    @NonNull\n    @Override\n    public Result doWork() {\n        if(!App.i().getUser().isAutoSync() || (App.i().getUser().isAutoSyncOnlyWifi() && !NetworkUtil.isWiFiUsed()) ){\n            return Result.success();\n        }\n        LiveEventBus.get(SyncWorker.SYNC_TASK_STATUS).post(true);\n        App.i().getApi().sync();\n        LiveEventBus.get(SyncWorker.SYNC_TASK_STATUS).post(false);\n        return Result.success();\n    }\n\n    @Override\n    public void onStopped() {\n        super.onStopped();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/api/AuthApi.java",
    "content": "package me.wizos.loread.network.api;\n\npublic abstract class AuthApi<T, E> extends BaseApi<T, E> {\n    private String authorization;\n    public void setAuthorization(String authorization){\n        this.authorization = authorization;\n    }\n    public String getAuthorization(){\n        return authorization;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/api/BaseApi.java",
    "content": "package me.wizos.loread.network.api;\n\nimport android.text.TextUtils;\nimport android.util.ArrayMap;\n\nimport com.socks.library.KLog;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.bean.feedly.input.EditFeed;\nimport me.wizos.loread.config.ArticleTags;\nimport me.wizos.loread.config.SaveDirectory;\nimport me.wizos.loread.config.Unsubscribe;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.db.ArticleTag;\nimport me.wizos.loread.db.Category;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.Feed;\nimport me.wizos.loread.db.FeedCategory;\nimport me.wizos.loread.db.Tag;\nimport me.wizos.loread.network.HttpClientManager;\nimport me.wizos.loread.network.callback.CallbackX;\nimport me.wizos.loread.utils.ArticleUtil;\nimport me.wizos.loread.utils.EncryptUtil;\nimport me.wizos.loread.utils.FileUtil;\nimport me.wizos.loread.utils.StringUtils;\nimport okhttp3.Call;\nimport okhttp3.Callback;\nimport okhttp3.Request;\n\n/**\n * Created by Wizos on 2019/2/10.\n */\n\npublic abstract class BaseApi<T, E> {\n    int fetchContentCntForEach = 20; // 每次获取内容的数量\n\n    // 同步所有数据，此处应该传入一个进度监听器，或者直接用EventBus发消息\n    abstract public void sync();\n\n    abstract public void fetchUserInfo(CallbackX cb);\n\n    abstract public void renameTag(String sourceTagId, String destTagId, CallbackX cb);\n\n    abstract public void editFeedCategories(List<E> lastCategoryItems, EditFeed editFeed, CallbackX cb);\n\n    abstract public void addFeed(EditFeed editFeed, CallbackX cb);\n\n    abstract public void unsubscribeFeed(String feedId, CallbackX cb);\n\n    abstract public void renameFeed(String feedId, String renamedTitle, CallbackX cb);\n\n    abstract public void markArticleReaded(String articleId, CallbackX cb);\n\n    abstract public void markArticleUnread(String articleId, CallbackX cb);\n\n    abstract public void markArticleStared(String articleId, CallbackX cb);\n\n    abstract public void markArticleUnstar(String articleId, CallbackX cb);\n\n    abstract public void markArticleListReaded(List<String> articleIds, CallbackX cb);\n\n    public interface ArticleChanger {\n        Article change(Article article);\n    }\n\n\n    void deleteExpiredArticles() {\n        // 最后的 300 * 1000L 是留前5分钟时间的不删除 WithPref.i().getClearBeforeDay()\n        long time = System.currentTimeMillis() - App.i().getUser().getCachePeriod() * 24 * 3600 * 1000L - 300 * 1000L;\n        String uid = App.i().getUser().getId();\n\n        List<Article> boxReadArts = CoreDB.i().articleDao().getReadedUnstarBeFiledLtTime(uid, time);\n        //KLog.i(\"移动文章\" + boxReadArts.size());\n        for (Article article : boxReadArts) {\n            article.setSaveStatus(App.STATUS_IS_FILED);\n            String dir = \"/\" + SaveDirectory.i().getSaveDir(article.getFeedId(),article.getId()) + \"/\";\n            //KLog.e(\"保存目录：\" + dir);\n            FileUtil.saveArticle(App.i().getUserBoxPath() + dir, article);\n        }\n        CoreDB.i().articleDao().update(boxReadArts);\n\n        List<Article> storeReadArts = CoreDB.i().articleDao().getReadedStaredBeFiledLtTime(uid, time);\n        //KLog.i(\"移动文章\" + storeReadArts.size());\n        for (Article article : storeReadArts) {\n            article.setSaveStatus(App.STATUS_IS_FILED);\n            String dir = \"/\" + SaveDirectory.i().getSaveDir(article.getFeedId(),article.getId()) + \"/\";\n            //KLog.e(\"保存目录：\" + dir);\n            FileUtil.saveArticle(App.i().getUserStorePath() + dir, article);\n        }\n        CoreDB.i().articleDao().update(storeReadArts);\n\n        List<Article> expiredArticles = CoreDB.i().articleDao().getReadedUnstarLtTime(uid, time);\n        ArrayList<String> idListMD5 = new ArrayList<>(expiredArticles.size());\n        for (Article article : expiredArticles) {\n            idListMD5.add(EncryptUtil.MD5(article.getId()));\n        }\n        //KLog.i(\"清除A：\" + time + \"--\" + expiredArticles.size());\n        FileUtil.deleteHtmlDirList(idListMD5);\n        CoreDB.i().articleDao().delete(expiredArticles);\n    }\n\n    void fetchReadability(String uid, long syncTimeMillis){\n        List<Article> articles = CoreDB.i().articleDao().getNeedReadability(uid,syncTimeMillis);\n        for (Article article : articles) {\n            //KLog.e(\"====获取：\" + entry.getKey() + \" , \" + article.getTitle() + \" , \" + article.getLink());\n            if (TextUtils.isEmpty(article.getLink())) {\n                continue;\n            }\n            //KLog.e(\"====开始请求\" );\n            Request request = new Request.Builder().url(article.getLink()).build();\n            Call call = HttpClientManager.i().simpleClient().newCall(request);\n            call.enqueue(new Callback() {\n                @Override\n                public void onFailure(@NotNull Call call, IOException e) {\n                    KLog.e(\"获取失败\");\n                }\n\n                // 在Android应用中直接使用上述代码进行异步请求，并且在回调方法中操作了UI，那么你的程序就会抛出异常，并且告诉你不能在非UI线程中操作UI。\n                // 这是因为OkHttp对于异步的处理仅仅是开启了一个线程，并且在线程中处理响应。\n                // OkHttp是一个面向于Java应用而不是特定平台(Android)的框架，那么它就无法在其中使用Android独有的Handler机制。\n                @Override\n                public void onResponse(@NotNull Call call, @NotNull okhttp3.Response response) throws IOException {\n                    if (response.isSuccessful()) {\n//                        Pattern pattern = Pattern.compile(\"</([a-zA-Z0-9]{1,10})>\", Pattern.CASE_INSENSITIVE);\n//                        String content = pattern.matcher(article.getContent()).replaceAll(\"_|_|_</$1>\");\n//                        content = Jsoup.parseBodyFragment(content).text();\n//                        String[] flags = content.split(\"_\\\\|_\\\\|_\");\n//                        String flag = \"\";\n//                        if(flags.length >= 1){\n//                            flag = flags[0];\n//                            if(flag.length() > 8){\n//                                flag = flag.substring(0,8);\n//                            }\n//                        }\n                        Article optimizedArticle = ArticleUtil.getReadabilityArticle(article,response.body());\n                        CoreDB.i().articleDao().update(optimizedArticle);\n                    }\n                }\n            });\n        }\n    }\n\n\n    void handleNotTagStarArticles(String uid, long syncTimeMillis){\n        List<Article> articles = CoreDB.i().articleDao().getNotTagStar(uid,syncTimeMillis);\n        List<ArticleTag> articleTags = new ArrayList<>();\n        Set<String> tagTitleSet = new HashSet<>();\n        for (Article article: articles){\n            if(StringUtils.isEmpty(article.getFeedId())){\n                continue;\n            }\n            List<Category> categories = CoreDB.i().categoryDao().getByFeedId(uid,article.getFeedId());\n            for (Category category:categories) {\n                articleTags.add( new ArticleTag(uid, article.getId(), category.getId()) );\n                tagTitleSet.add(category.getTitle());\n            }\n        }\n        CoreDB.i().articleTagDao().insert(articleTags);\n\n        List<Tag> tags = new ArrayList<>(tagTitleSet.size());\n        for (String title:tagTitleSet) {\n            Tag tag = new Tag();\n            tag.setUid(uid);\n            tag.setId(title);\n            tag.setTitle(title);\n            tags.add(tag);\n            KLog.e(\"设置 Tag 数据：\" + tag);\n        }\n        CoreDB.i().tagDao().insert(tags);\n        CoreDB.i().articleTagDao().insert(articleTags);\n        ArticleTags.i().addArticleTags(articleTags);\n        ArticleTags.i().save();\n    }\n\n    void clearNotArticleTags(String uid){\n        List<ArticleTag> articleTags = CoreDB.i().articleTagDao().getNotArticles(uid);\n        for (ArticleTag articleTag:articleTags) {\n            CoreDB.i().articleTagDao().delete(articleTag);\n        }\n    }\n\n    void coverSaveCategories(List<Category> cloudyCategories) {\n        String uid = App.i().getUser().getId();\n        ArrayMap<String, Category> cloudyCategoriesTmp = new android.util.ArrayMap<>(cloudyCategories.size());\n        for (Category category : cloudyCategories) {\n            category.setUid(uid);\n            cloudyCategoriesTmp.put(category.getId(), category);\n        }\n\n        List<Category> localCategories = CoreDB.i().categoryDao().getAll(uid);\n        Iterator<Category> iterator = localCategories.iterator();\n        Category tmpCategory;\n        while(iterator.hasNext()){\n            tmpCategory = iterator.next();\n            if (cloudyCategoriesTmp.get(tmpCategory.getId()) == null) {\n                CoreDB.i().categoryDao().delete(tmpCategory);\n                iterator.remove();\n            }else {\n                cloudyCategoriesTmp.remove(tmpCategory.getId());\n            }\n        }\n\n        cloudyCategories.clear();\n        for (Map.Entry<String,Category> entry: cloudyCategoriesTmp.entrySet()) {\n            cloudyCategories.add(entry.getValue());\n        }\n\n        CoreDB.i().categoryDao().insert(localCategories);\n        CoreDB.i().categoryDao().insert(cloudyCategories);\n    }\n\n    void coverSaveFeeds(List<Feed> cloudyFeeds) {\n        String uid = App.i().getUser().getId();\n        ArrayMap<String, Feed> cloudyMap = new android.util.ArrayMap<>(cloudyFeeds.size());\n        for (Feed feed : cloudyFeeds) {\n            feed.setUid(uid);\n            cloudyMap.put(feed.getId(), feed);\n        }\n\n        List<Feed> localFeeds = CoreDB.i().feedDao().getAll(uid);\n        List<Feed> deleteFeeds = new ArrayList<>();\n        Iterator<Feed> iterator = localFeeds.iterator();\n        Feed localFeed, commonFeed;\n        while(iterator.hasNext()){\n            localFeed = iterator.next();\n            commonFeed = cloudyMap.get(localFeed.getId());\n            if ( commonFeed == null) {\n                CoreDB.i().feedCategoryDao().deleteByFeedId(localFeed.getUid(), localFeed.getId());\n                CoreDB.i().articleDao().deleteUnStarByFeedId(localFeed.getUid(), localFeed.getId());\n                CoreDB.i().feedDao().delete(localFeed);\n                deleteFeeds.add(localFeed);\n                iterator.remove();// 删除后，这里只剩2者的交集\n//                KLog.e(\"删除本地的feed：\" + localFeed.getId() + \" , \" + localFeed.getTitle() + \" , \" + localFeed.getUnreadCount() );\n            }else {\n                localFeed.setTitle(commonFeed.getTitle());\n                localFeed.setFeedUrl(commonFeed.getFeedUrl());\n                localFeed.setHtmlUrl(commonFeed.getHtmlUrl());\n                cloudyMap.remove(localFeed.getId());\n            }\n        }\n        cloudyFeeds.clear();\n        for (Map.Entry<String,Feed> entry: cloudyMap.entrySet()) {\n            cloudyFeeds.add(entry.getValue());\n        }\n\n        CoreDB.i().feedDao().insert(localFeeds);\n        CoreDB.i().feedDao().insert(cloudyFeeds);\n        Unsubscribe.genBackupFile2(App.i().getUser(), deleteFeeds);\n    }\n\n    void coverFeedCategory(List<FeedCategory> cloudyFeedCategories) {\n        ArrayMap<String, FeedCategory> cloudyCategoriesTmp = new ArrayMap<>(cloudyFeedCategories.size());\n        for (FeedCategory feedCategory : cloudyFeedCategories) {\n            cloudyCategoriesTmp.put(feedCategory.getFeedId() + feedCategory.getCategoryId(), feedCategory);\n        }\n\n        List<FeedCategory> localFeedCategories =  CoreDB.i().feedCategoryDao().getAll(App.i().getUser().getId());\n        FeedCategory tmp;\n\n        for (FeedCategory feedCategory : localFeedCategories) {\n            tmp = cloudyCategoriesTmp.get(feedCategory.getFeedId() + feedCategory.getCategoryId());\n            if (tmp == null) {\n                CoreDB.i().feedCategoryDao().delete(feedCategory);\n            } else {\n                cloudyFeedCategories.remove(tmp);\n            }\n        }\n        CoreDB.i().feedCategoryDao().insert(cloudyFeedCategories);\n    }\n\n\n\n    void handleDuplicateArticle() {\n        // 清理重复的文章\n        Article articleSample;\n        List<String> links = CoreDB.i().articleDao().getDuplicatesLink(App.i().getUser().getId());\n        List<Article> articleList;\n        for (String link : links) {\n            articleList = CoreDB.i().articleDao().getDuplicates(App.i().getUser().getId(), link);\n            if (articleList == null || articleList.size() == 0) {\n                continue;\n            }\n            // KLog.e(\"获取到的重复文章数量：\" + articleList.size());\n            // 获取第一个作为范例\n            articleSample = articleList.get(0);\n            articleList.remove(0);\n\n            List<Article> articles = new ArrayList<>();\n            for (Article article: articleList) {\n                if( articleSample.getCrawlDate() != article.getCrawlDate()){\n                    article.setCrawlDate(articleSample.getCrawlDate());\n                    article.setPubDate(articleSample.getPubDate());\n                    articles.add(article);\n                }\n            }\n            CoreDB.i().articleDao().update(articles);\n        }\n    }\n\n    // 优化在使用状态下多次同步到新文章时，这些文章的爬取时间\n    void handleCrawlDate(){\n        String uid = App.i().getUser().getId();\n        long lastReadMarkTimeMillis = CoreDB.i().articleDao().getLastReadTimeMillis(uid);\n        long lastStarMaskTimeMillis = CoreDB.i().articleDao().getLastStarTimeMillis(uid);\n        long lastMarkTimeMillis = Math.max(lastReadMarkTimeMillis,lastStarMaskTimeMillis);\n        CoreDB.i().articleDao().updateIdleCrawlDate(uid, lastMarkTimeMillis, System.currentTimeMillis());\n    }\n\n    void updateCollectionCount() {\n        String uid = App.i().getUser().getId();\n        CoreDB.i().feedDao().update( CoreDB.i().feedDao().getFeedsRealTimeCount(uid) );\n        CoreDB.i().categoryDao().update( CoreDB.i().categoryDao().getCategoriesRealTimeCount(uid) );\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/api/FeedlyApi.java",
    "content": "package me.wizos.loread.network.api;\n\nimport android.text.TextUtils;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.collection.ArrayMap;\n\nimport com.google.gson.GsonBuilder;\nimport com.hjq.toast.ToastUtils;\nimport com.jeremyliao.liveeventbus.LiveEventBus;\nimport com.lzy.okgo.exception.HttpException;\nimport com.socks.library.KLog;\n\nimport java.io.IOException;\nimport java.net.ConnectException;\nimport java.net.SocketTimeoutException;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.bean.Token;\nimport me.wizos.loread.bean.feedly.CategoryItem;\nimport me.wizos.loread.bean.feedly.Collection;\nimport me.wizos.loread.bean.feedly.Entry;\nimport me.wizos.loread.bean.feedly.FeedItem;\nimport me.wizos.loread.bean.feedly.Profile;\nimport me.wizos.loread.bean.feedly.StreamIds;\nimport me.wizos.loread.bean.feedly.input.EditCollection;\nimport me.wizos.loread.bean.feedly.input.EditFeed;\nimport me.wizos.loread.bean.feedly.input.MarkerAction;\nimport me.wizos.loread.config.ArticleActionConfig;\nimport me.wizos.loread.config.LinkRewriteConfig;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.db.Category;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.Feed;\nimport me.wizos.loread.db.FeedCategory;\nimport me.wizos.loread.db.User;\nimport me.wizos.loread.network.HttpClientManager;\nimport me.wizos.loread.network.SyncWorker;\nimport me.wizos.loread.network.callback.CallbackX;\nimport me.wizos.loread.utils.StringUtils;\nimport okhttp3.FormBody;\nimport retrofit2.Response;\nimport retrofit2.Retrofit;\nimport retrofit2.converter.gson.GsonConverterFactory;\n\nimport static me.wizos.loread.utils.StringUtils.getString;\n\n/**\n * Created by Wizos on 2019/2/8.\n */\n\npublic class FeedlyApi extends OAuthApi<Feed, CategoryItem> {\n    private static final String APP_ID = \"palabre\";\n    private static final String APP_KEY = \"FE01H48LRK62325VQVGYOZ24YFZL\";\n    private static final String OFFICIAL_BASE_URL = \"https://feedly.com/v3\";\n    private static final String REDIRECT_URI = \"palabre://feedlyauth\";\n\n    // 系统默认的分类\n    // user/12cc057f-9891-4ab3-99da-86f2dee7f2f5/category/global.must\n    // user/12cc057f-9891-4ab3-99da-86f2dee7f2f5/category/global.uncategorized\n    // user/12cc057f-9891-4ab3-99da-86f2dee7f2f5/category/global.all\n    // user/12cc057f-9891-4ab3-99da-86f2dee7f2f5/tag/global.unsaved // 取消稍后读\n    // user/12cc057f-9891-4ab3-99da-86f2dee7f2f5/tag/global.saved // 稍后读（加星）\n    // user/12cc057f-9891-4ab3-99da-86f2dee7f2f5/tag/杂 // tag\n\n    private Retrofit retrofit;\n    private FeedlyService service;\n\n    public FeedlyApi() {\n        String baseUrl = LinkRewriteConfig.i().getRedirectUrl(FeedlyApi.OFFICIAL_BASE_URL);\n        if(StringUtils.isEmpty(baseUrl)){\n            baseUrl = FeedlyApi.OFFICIAL_BASE_URL;\n        }\n        retrofit = new Retrofit.Builder()\n                .baseUrl(baseUrl + \"/\") // 设置网络请求的Url地址, 必须以/结尾\n                .addConverterFactory(GsonConverterFactory.create(new GsonBuilder().setLenient().create()))  // 设置数据解析器\n                .client(HttpClientManager.i().feedlyHttpClient())\n                .build();\n        service = retrofit.create(FeedlyService.class);\n    }\n\n    @Override\n    public void setAuthorization(String authorization) {\n        super.setAuthorization(authorization);\n    }\n\n    public String getOAuthUrl() {\n        String baseUrl = LinkRewriteConfig.i().getRedirectUrl(OFFICIAL_BASE_URL);\n        if(StringUtils.isEmpty(baseUrl)){\n            baseUrl = OFFICIAL_BASE_URL;\n        }\n\n        if (!baseUrl.endsWith(\"/\")) {\n            baseUrl = baseUrl + \"/\";\n        }\n        return baseUrl + \"auth/auth?response_type=code&client_id=palabre&scope=https://cloud.feedly.com/subscriptions&redirect_uri=palabre://feedlyauth&state=/profile\";\n//        String redirectUri = \"loread://oauth\";\n//        String url = \"https://cloud.feedly.com/v3/auth/auth?response_type=code&client_id=\" + clientId + \"&scope=https://cloud.feedly.com/subscriptions&redirect_uri=\" + redirectUri + \"&state=/profile\";\n//        return HOST + \"/auth/auth?response_type=code&client_id=\" + APP_ID + \"&redirect_uri=\" + redirectUri + \"&state=loread&scope=https://cloud.feedly.com/subscriptions\";\n    }\n    public void getAccessToken(String authorizationCode,CallbackX cb) {\n        FormBody.Builder builder = new FormBody.Builder();\n        builder.add(\"grant_type\", \"authorization_code\");\n        builder.add(\"code\", authorizationCode);\n        builder.add(\"redirect_uri\", REDIRECT_URI);\n        builder.add(\"client_id\", APP_ID);\n        builder.add(\"client_secret\", APP_KEY);\n\n        service.getAccessToken(\"authorization_code\", REDIRECT_URI,APP_ID,APP_KEY,authorizationCode).enqueue(new retrofit2.Callback<Token>() {\n            @Override\n            public void onResponse(retrofit2.Call<Token> call, Response<Token> response) {\n                if(response.isSuccessful()){\n                    cb.onSuccess(response.body());\n                }else {\n                    cb.onFailure(\"失败：\" + response.message());\n                }\n            }\n\n            @Override\n            public void onFailure(retrofit2.Call<Token> call, Throwable t) {\n                cb.onFailure(\"失败：\" + t.getMessage());\n            }\n        });\n    }\n\n\n    public void refreshingAccessToken(String refreshToken, CallbackX cb) {\n        service.refreshingAccessToken(\"refresh_token\",refreshToken,APP_ID,APP_KEY).enqueue(new retrofit2.Callback<Token>() {\n            @Override\n            public void onResponse(retrofit2.Call<Token> call, Response<Token> response) {\n                if(response.isSuccessful() && response.body()!=null){\n                    if (TextUtils.isEmpty(response.body().getRefresh_token())) {\n                        response.body().setRefresh_token(refreshToken);\n                    }\n                    User user = App.i().getUser();\n                    if (user != null) {\n                        user.setToken(response.body());\n                        CoreDB.i().userDao().insert(user);\n                    }\n                    // 更新缓存中的授权\n                    ((FeedlyApi) App.i().getApi()).setAuthorization(App.i().getUser().getAuth());\n\n                    cb.onSuccess(response.body());\n                }else {\n                    cb.onFailure(\"失败：\" + response.message());\n                }\n            }\n\n            @Override\n            public void onFailure(retrofit2.Call<Token> call, Throwable t) {\n                cb.onFailure(\"失败：\" + t.getMessage());\n            }\n        });\n    }\n    public String refreshingAccessToken(String refreshToken) throws IOException {\n        Token token = service.refreshingAccessToken(\"refresh_token\",refreshToken,APP_ID,APP_KEY).execute().body();\n        if (TextUtils.isEmpty(token.getRefresh_token())) {\n            token.setRefresh_token(refreshToken);\n        }\n        User user = App.i().getUser();\n        if (user != null) {\n            user.setToken(token);\n            CoreDB.i().userDao().insert(user);\n        }\n        // 更新缓存中的授权\n        ((FeedlyApi) App.i().getApi()).setAuthorization(App.i().getUser().getAuth());\n        return token.getAuth();\n    }\n\n    public void fetchUserInfo(CallbackX cb){\n        service.getUserInfo(getAuthorization()).enqueue(new retrofit2.Callback<Profile>() {\n            @Override\n            public void onResponse(@NonNull retrofit2.Call<Profile> call,@NonNull Response<Profile> response) {\n                KLog.e(\"获取资料：\" + response.isSuccessful() );\n                if( response.isSuccessful()){\n                    cb.onSuccess(response.body().getUser());\n                }else {\n                    cb.onFailure(\"获取失败：\" + response.message());\n                }\n            }\n\n            @Override\n            public void onFailure(@NonNull retrofit2.Call<Profile> call,@NonNull Throwable t) {\n                cb.onFailure(\"获取失败：\" + t.getMessage());\n            }\n        });\n    }\n\n    @Override\n    public void sync() {\n        try {\n            long startSyncTimeMillis = System.currentTimeMillis();\n            String uid = App.i().getUser().getId();\n\n            KLog.e(\"3 - 同步订阅源信息\");\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post(getString(R.string.sync_feed_info));\n\n            // 获取分类&feed\n            List<Collection> collectionList = service.getCollections(getAuthorization()).execute().body();\n            Iterator<Collection> collectionsIterator = collectionList.iterator();\n            Collection collection;\n            // 本地无该 category\n            ArrayList<FeedItem> feedItems;\n            ArrayList<Category> categories = new ArrayList<>();\n            CategoryItem categoryItem;\n            Feed feed;\n            Category category;\n            ArrayList<FeedCategory> feedCategories = new ArrayList<>();\n            ArrayList<Feed> feeds = new ArrayList<>();\n\n            while (collectionsIterator.hasNext()) {\n                collection = collectionsIterator.next();\n                if (collection.getId().endsWith(App.CATEGORY_MUST)) {\n                    collectionsIterator.remove();   //注意这个地方\n                    continue;\n                }\n                categoryItem = collection.getCategoryItem();\n                feedItems = collection.getFeedItems();\n\n                category = categoryItem.convert();\n                category.setUid(uid);\n                categories.add(category);\n                for (FeedItem feedItemTmp : feedItems) {\n                    feed = feedItemTmp.convert2Feed();\n                    feed.setUid(uid);\n                    feeds.add(feed);\n                    feedCategories.add( new FeedCategory(uid, feedItemTmp.getId(), categoryItem.getId()) );\n                }\n            }\n\n\n            // 如果在获取到数据的时候就保存，那么到这里同步断了的话，可能系统内的文章就找不到响应的分组，所有放到这里保存。\n            // 覆盖保存，只会保留最新一份。（比如在云端将文章移到的新的分组）\n            coverSaveFeeds(feeds);\n            coverSaveCategories(categories);\n            coverFeedCategory(feedCategories);\n\n            KLog.e(\" 2 - 同步未读，加星文章的ids\");\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post(getString(R.string.sync_article_refs));\n\n            StreamIds tempStreamIds;\n            List<String> cloudyRefs;\n            // 获取未读资源\n            tempStreamIds = new StreamIds();\n            cloudyRefs = new ArrayList<>();\n            do {\n                tempStreamIds = service.getUnreadRefs(getAuthorization(),\"user/\" + App.i().getUser().getUserId() + App.CATEGORY_ALL, 10000, true, tempStreamIds.getContinuation()).execute().body();\n                cloudyRefs.addAll(tempStreamIds.getIds());\n            } while (tempStreamIds.getContinuation() != null);\n            HashSet<String> unreadRefsList = handleUnreadRefs(cloudyRefs);\n\n            // 获取加星资源\n            tempStreamIds = new StreamIds();\n            cloudyRefs = new ArrayList<>();\n            do {\n                tempStreamIds = service.getStarredRefs(getAuthorization(),\"user/\" + App.i().getUser().getUserId() + App.CATEGORY_STARED, 10000, tempStreamIds.getContinuation()).execute().body();\n                cloudyRefs.addAll(tempStreamIds.getIds());\n            } while (tempStreamIds.getContinuation() != null);\n            HashSet<String> staredRefsList = handleStaredRefs(cloudyRefs);\n\n\n            ArrayList<HashSet<String>> refsList = splitRefs(unreadRefsList, staredRefsList);\n            int allSize = refsList.get(0).size() + refsList.get(1).size() + refsList.get(2).size();\n\n            KLog.e(\"1 - 同步文章内容\");\n\n            // 抓取【未读、未加星】文章\n            fetchArticle(allSize, 0, new ArrayList<>(refsList.get(0)), new ArticleChanger() {\n                @Override\n                public Article change(Article article) {\n                    article.setCrawlDate(System.currentTimeMillis());\n                    article.setReadStatus(App.STATUS_UNREAD);\n                    article.setStarStatus(App.STATUS_UNSTAR);\n                    article.setUid(uid);\n                    return article;\n                }\n            });\n            // 抓取【已读、已加星】文章\n            fetchArticle(allSize, refsList.get(0).size(), new ArrayList<>(refsList.get(1)), new ArticleChanger() {\n                @Override\n                public Article change(Article article) {\n                    article.setCrawlDate(System.currentTimeMillis());\n                    article.setReadStatus(App.STATUS_READED);\n                    article.setStarStatus(App.STATUS_STARED);\n                    article.setUid(uid);\n                    return article;\n                }\n            });\n\n            // 抓取【未读、已加星】文章\n            fetchArticle(allSize, refsList.get(0).size() + refsList.get(1).size(), new ArrayList<>(refsList.get(2)), new ArticleChanger() {\n                @Override\n                public Article change(Article article) {\n                    article.setCrawlDate(System.currentTimeMillis());\n                    article.setReadStatus(App.STATUS_UNSTAR);\n                    article.setStarStatus(App.STATUS_STARED);\n                    article.setUid(uid);\n                    return article;\n                }\n            });\n\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post(getString(R.string.clear_article));\n            deleteExpiredArticles();\n            handleDuplicateArticle();\n            handleCrawlDate();\n            updateCollectionCount();\n\n            // 获取文章全文\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post(getString(R.string.fetch_article_full_content));\n            fetchReadability(uid, startSyncTimeMillis);\n\n            // 为所有新增的加星文章自动生成tag\n            handleNotTagStarArticles(uid, startSyncTimeMillis);\n            // 执行文章自动处理脚本\n            ArticleActionConfig.i().exeRules(uid,startSyncTimeMillis);\n            // 清理无文章的tag\n            //clearNotArticleTags(uid);\n\n            // 提示更新完成\n            LiveEventBus.get(SyncWorker.NEW_ARTICLE_NUMBER).post(allSize);\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post( null );\n        } catch (HttpException e) {\n            KLog.e(\"同步时产生HttpException：\" + e.message());\n            e.printStackTrace();\n            handleException(e);\n        } catch (ConnectException e) {\n            KLog.e(\"同步时产生异常ConnectException\");\n            e.printStackTrace();\n            handleException(e);\n        } catch (SocketTimeoutException e) {\n            KLog.e(\"同步时产生异常SocketTimeoutException\");\n            e.printStackTrace();\n            handleException(e);\n        } catch (IOException e) {\n            KLog.e(\"同步时产生异常IOException\");\n            e.printStackTrace();\n            handleException(e);\n        } catch (RuntimeException e) {\n            KLog.e(\"同步时产生异常RuntimeException\");\n            e.printStackTrace();\n            handleException(e);\n        }\n    }\n\n    private void handleException(Exception e) {\n        if (e instanceof HttpException) {\n            ToastUtils.show(\"网络异常：\" + e.getMessage());\n        } else {\n            updateCollectionCount();\n        }\n\n        LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post( null );\n    }\n\n    public void renameTag(String tagId, String targetName, CallbackX cb) {\n        editTag(tagId, targetName,  cb);\n    }\n\n    private void editTag(@Nullable String tagId, @Nullable String targetName, CallbackX cb) {\n        EditCollection editCollection = new EditCollection(tagId);\n        if (!TextUtils.isEmpty(tagId)) {\n            editCollection.setId(tagId);\n        }\n        if (!TextUtils.isEmpty(targetName)) {\n            editCollection.setLabel(targetName);\n        }\n\n        service.editCollections(getAuthorization(),editCollection).enqueue(new retrofit2.Callback<List<Collection>>() {\n            @Override\n            public void onResponse(retrofit2.Call<List<Collection>> call, retrofit2.Response<List<Collection>> response) {\n                if (response.isSuccessful()) {\n                    KLog.e(\"修改成功\" + response.body().toString());\n                    cb.onSuccess(null);\n                } else {\n                    cb.onFailure(null);\n                }\n            }\n\n            @Override\n            public void onFailure(retrofit2.Call<List<Collection>> call, Throwable t) {\n                cb.onFailure(\"修改失败\" + t.getMessage());\n                KLog.e(\"修改失败\");\n            }\n        });\n    }\n\n    @Override\n    public void addFeed(EditFeed editFeed, CallbackX cb) {\n        service.editFeed(getAuthorization(),editFeed).enqueue(new retrofit2.Callback<List<EditFeed>>() {\n            @Override\n            public void onResponse(retrofit2.Call<List<EditFeed>> call, retrofit2.Response<List<EditFeed>> response) {\n                if (response.isSuccessful()) {\n                    KLog.e(\"添加成功\" + response.body().toString());\n                    cb.onSuccess(null);\n                } else {\n                    cb.onFailure(null);\n                }\n            }\n\n            @Override\n            public void onFailure(retrofit2.Call<List<EditFeed>> call, Throwable t) {\n                cb.onFailure(t.getMessage());\n                KLog.e(\"添加失败\");\n            }\n        });\n    }\n\n    @Override\n    public void renameFeed(String feedId, String feedTitle, CallbackX cb) {\n        //editFeed(feedId, renamedTitle, null, cb);\n        EditFeed editFeed = new EditFeed(feedId);\n        if (!TextUtils.isEmpty(feedTitle)) {\n            editFeed.setTitle(feedTitle);\n        }\n\n        service.editFeed(getAuthorization(),editFeed).enqueue(new retrofit2.Callback<List<EditFeed>>() {\n            @Override\n            public void onResponse(retrofit2.Call<List<EditFeed>> call, retrofit2.Response<List<EditFeed>> response) {\n                if(!response.isSuccessful()){\n                    cb.onFailure(\"修改失败\");\n                }else {\n                    cb.onSuccess(response);\n                }\n            }\n\n            @Override\n            public void onFailure(retrofit2.Call<List<EditFeed>> call, Throwable t) {\n                cb.onFailure(t.getMessage());\n            }\n        });\n    }\n\n    @Override\n    public void editFeedCategories(List<CategoryItem> lastCategoryItems, EditFeed editFeed, CallbackX cb) {\n        service.editFeed(getAuthorization(),editFeed).enqueue(new retrofit2.Callback<List<EditFeed>>() {\n            @Override\n            public void onResponse(retrofit2.Call<List<EditFeed>> call, retrofit2.Response<List<EditFeed>> response) {\n                if(!response.isSuccessful()){\n                    cb.onFailure(\"修改失败\");\n                }else {\n                    cb.onSuccess(null);\n                }\n            }\n\n            @Override\n            public void onFailure(retrofit2.Call<List<EditFeed>> call, Throwable t) {\n                cb.onFailure(t.getMessage());\n            }\n        });\n    }\n\n    public void unsubscribeFeed(String feedId, CallbackX cb) {\n        ArrayList<String> feedIds = new ArrayList<>();\n        feedIds.add(feedId);\n        service.delFeed(getAuthorization(),feedIds).enqueue(new retrofit2.Callback<String>() {\n            @Override\n            public void onResponse(retrofit2.Call<String> call, retrofit2.Response<String> response) {\n                if (response.isSuccessful() ){\n                    String msg = response.body();\n                    if( \"[]\".equals(msg) ){\n                        cb.onSuccess(null);\n                    }else {\n                        cb.onFailure(msg);\n                    }\n                }else {\n                    cb.onFailure(\"修改失败：原因未知\");\n                }\n            }\n\n            @Override\n            public void onFailure(retrofit2.Call<String> call, Throwable t) {\n\n            }\n        });\n    }\n\n    private void markArticles(String action, List<String> ids, CallbackX cb) {\n        MarkerAction markerAction = new MarkerAction();\n        markerAction.setAction(action);\n        markerAction.setType(MarkerAction.TYPE_ENTRIES);\n        markerAction.setEntryIds(ids);\n        // 成功不返回信息\n        service.markers(getAuthorization(),markerAction).enqueue(new retrofit2.Callback<String>() {\n            @Override\n            public void onResponse(retrofit2.Call<String> call, retrofit2.Response<String> response) {\n                if (response.isSuccessful() ){\n                    cb.onSuccess(null);\n                }else {\n                    cb.onFailure(\"修改失败：原因未知\");\n                }\n            }\n\n            @Override\n            public void onFailure(retrofit2.Call<String> call, Throwable t) {\n                cb.onFailure(\"修改失败：原因未知\");\n            }\n        });\n    }\n\n    private void markArticle(String action, String articleId, CallbackX cb) {\n        List<String> ids = new ArrayList<String>();\n        ids.add(articleId);\n        markArticles(action, ids,cb);\n    }\n    @Override\n    public void markArticleListReaded(List<String> articleIds, CallbackX cb) {\n        markArticles(MarkerAction.MARK_AS_READ, articleIds,cb);\n    }\n\n\n    public void markArticleReaded(String articleId,CallbackX cb) {\n        KLog.e(\"标记已读F：\" );\n        markArticle(MarkerAction.MARK_AS_READ, articleId, cb);\n    }\n\n    public void markArticleUnread(String articleId,CallbackX cb) {\n        markArticle(MarkerAction.MARK_AS_UNREAD, articleId, cb);\n    }\n\n    public void markArticleStared(String articleId,CallbackX cb) {\n        markArticle(MarkerAction.MARK_AS_SAVED, articleId, cb);\n    }\n\n    public void markArticleUnstar(String articleId,CallbackX cb) {\n        markArticle(MarkerAction.MARK_AS_UNSAVED, articleId, cb);\n    }\n\n\n    private void fetchArticle(int allSize, int syncedSize, List<String> subIds, ArticleChanger articleChanger) throws IOException{\n        int needFetchCount = subIds.size();\n        int hadFetchCount = 0;\n\n        while (needFetchCount > 0) {\n            int fetchUnit = Math.min(needFetchCount, fetchContentCntForEach);\n            List<Entry> items = service.getItemContents(getAuthorization(), subIds.subList(hadFetchCount, hadFetchCount = hadFetchCount + fetchUnit)).execute().body();\n            List<Article> tempArticleList = new ArrayList<>(fetchUnit);\n            for (Entry item : items) {\n                tempArticleList.add(item.convert(articleChanger));\n            }\n            CoreDB.i().articleDao().insert(tempArticleList);\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post( App.i().getString(R.string.sync_article_content, syncedSize = syncedSize + fetchUnit, allSize) );\n            needFetchCount = subIds.size() - hadFetchCount;\n        }\n    }\n\n    private HashSet<String> handleUnreadRefs(List<String> ids) {\n        //List<Article> localUnreadArticles = WithDB.i().getArtsUnreadNoOrder();\n        List<Article> localUnreadArticles = CoreDB.i().articleDao().getUnreadNoOrder(App.i().getUser().getId());\n        Map<String, Article> localUnreadArticlesMap = new ArrayMap<>(localUnreadArticles.size());\n        List<Article> changedArticles = new ArrayList<>();\n        // 筛选下来，最终要去云端获取内容的未读Refs的集合\n        HashSet<String> tempUnreadIds = new HashSet<>(ids.size());\n        // 数据量大的一方\n        for (Article article : localUnreadArticles) {\n            localUnreadArticlesMap.put(article.getId(), article);\n        }\n        // 数据量小的一方\n        Article article;\n        for (String articleId : ids) {\n            article = localUnreadArticlesMap.get(articleId);\n            if (article != null) {\n                localUnreadArticlesMap.remove(articleId);\n            } else {\n                article = CoreDB.i().articleDao().getById(App.i().getUser().getId(), articleId);\n                if (article != null && article.getReadStatus() == App.STATUS_READED) {\n                    article.setReadStatus(App.STATUS_UNREAD);\n                    changedArticles.add(article);\n                } else {\n                    // 本地无，而云端有，加入要请求的未读资源\n                    tempUnreadIds.add(articleId);\n                }\n            }\n        }\n        for (Map.Entry<String, Article> entry : localUnreadArticlesMap.entrySet()) {\n            if (entry.getKey() != null) {\n                article = localUnreadArticlesMap.get(entry.getKey());\n                // 本地未读设为已读\n                article.setReadStatus(App.STATUS_READED);\n                changedArticles.add(article);\n            }\n        }\n\n        CoreDB.i().articleDao().update(changedArticles);\n        return tempUnreadIds;\n    }\n\n    private HashSet<String> handleStaredRefs(List<String> streamIds) {\n//        List<Article> localStarredArticles = WithDB.i().getArtsStared();\n        List<Article> localStarredArticles = CoreDB.i().articleDao().getStaredNoOrder(App.i().getUser().getId());\n        Map<String, Article> localStarredArticlesMap = new ArrayMap<>(localStarredArticles.size());\n        List<Article> changedArticles = new ArrayList<>();\n        HashSet<String> tempStarredIds = new HashSet<>(streamIds.size());\n\n        // 第1步，遍历数据量大的一方A，将其比对项目放入Map中\n        for (Article article : localStarredArticles) {\n            localStarredArticlesMap.put(article.getId(), article);\n        }\n\n        // 第2步，遍历数据量小的一方B。到Map中找，是否含有b中的比对项。有则XX，无则YY\n        Article article;\n        for (String articleId : streamIds) {\n            article = localStarredArticlesMap.get(articleId);\n            if (article != null) {\n                localStarredArticlesMap.remove(articleId);\n            } else {\n//                article = WithDB.i().getArticle(articleId);\n                article = CoreDB.i().articleDao().getById(App.i().getUser().getId(), articleId);\n                if (article != null) {\n                    article.setStarStatus(App.STATUS_STARED);\n                    changedArticles.add(article);\n                } else {\n                    // 本地无，而云远端有，加入要请求的未读资源\n                    tempStarredIds.add(articleId);\n                }\n            }\n        }\n\n        for (Map.Entry<String, Article> entry : localStarredArticlesMap.entrySet()) {\n            if (entry.getKey() != null) {\n                article = localStarredArticlesMap.get(entry.getKey());\n                article.setStarStatus(App.STATUS_UNSTAR);\n                changedArticles.add(article);// 取消加星\n            }\n        }\n\n        CoreDB.i().articleDao().update(changedArticles);\n        return tempStarredIds;\n    }\n\n    /**\n     * 将 未读资源 和 加星资源，去重分为3组\n     *\n     * @param tempUnreadIds\n     * @param tempStarredIds\n     * @return\n     */\n    private ArrayList<HashSet<String>> splitRefs(HashSet<String> tempUnreadIds, HashSet<String> tempStarredIds) {\n//        KLog.e(\"【reRefs1】云端未读\" + tempUnreadIds.size() + \"，云端加星\" + tempStarredIds.size());\n        int total = Math.min(tempUnreadIds.size(), tempStarredIds.size());\n\n        HashSet<String> reUnreadUnstarRefs;\n        HashSet<String> reReadStarredRefs = new HashSet<>(tempStarredIds.size());\n        HashSet<String> reUnreadStarredRefs = new HashSet<>(total);\n\n        for (String id : tempStarredIds) {\n            if (tempUnreadIds.contains(id)) {\n                tempUnreadIds.remove(id);\n                reUnreadStarredRefs.add(id);\n            } else {\n                reReadStarredRefs.add(id);\n            }\n        }\n        reUnreadUnstarRefs = tempUnreadIds;\n\n        ArrayList<HashSet<String>> refsList = new ArrayList<>();\n        refsList.add(reUnreadUnstarRefs);\n        refsList.add(reReadStarredRefs);\n        refsList.add(reUnreadStarredRefs);\n//        KLog.e(\"【reRefs2】\" + reUnreadUnstarRefs.size() + \"--\" + reReadStarredRefs.size() + \"--\" + reUnreadStarredRefs.size());\n        return refsList;\n    }\n\n    private ArrayMap<String, ArrayList<Article>> getFeedArticleNeedReadability(ArrayMap<String, ArrayList<Article>> feedIds, ArrayList<Article> articles) {\n        if (null == feedIds) {\n            return new ArrayMap<>();\n        }\n        if (articles != null) {\n            ArrayList<Article> arrayList;\n            for (Article article : articles) {\n                arrayList = feedIds.get(article.getFeedId());\n                if (null == arrayList) {\n                    arrayList = new ArrayList<Article>();\n                    arrayList.add(article);\n                    feedIds.put(article.getFeedId(), arrayList);\n                } else {\n                    arrayList.add(article);\n                }\n            }\n        }\n        return feedIds;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/api/FeedlyService.java",
    "content": "package me.wizos.loread.network.api;\n\nimport androidx.annotation.NonNull;\n\nimport java.util.List;\n\nimport me.wizos.loread.bean.Token;\nimport me.wizos.loread.bean.feedly.Collection;\nimport me.wizos.loread.bean.feedly.Entry;\nimport me.wizos.loread.bean.feedly.FeedItem;\nimport me.wizos.loread.bean.feedly.Profile;\nimport me.wizos.loread.bean.feedly.StreamContents;\nimport me.wizos.loread.bean.feedly.StreamIds;\nimport me.wizos.loread.bean.feedly.input.EditCollection;\nimport me.wizos.loread.bean.feedly.input.EditFeed;\nimport me.wizos.loread.bean.feedly.input.MarkerAction;\nimport me.wizos.loread.bean.search.SearchFeeds;\nimport retrofit2.Call;\nimport retrofit2.http.Body;\nimport retrofit2.http.Field;\nimport retrofit2.http.FormUrlEncoded;\nimport retrofit2.http.GET;\nimport retrofit2.http.HTTP;\nimport retrofit2.http.Header;\nimport retrofit2.http.Headers;\nimport retrofit2.http.POST;\nimport retrofit2.http.Path;\nimport retrofit2.http.Query;\n\n/**\n * Created by Wizos on 2019/4/13.\n */\n\npublic interface FeedlyService {\n    // Post请求的文本参数则用注解@Field来声明，同时还必须给方法添加注解@FormUrlEncoded来告知Retrofit参数为表单参数，如果只为参数增加@Field注解，而不给方法添加@FormUrlEncoded注解运行时会抛异常。\n    @FormUrlEncoded\n    @POST(\"auth/token\")\n    Call<Token> getAccessToken(\n            @Field(\"grant_type\") String grantType,\n            @Field(\"redirect_uri\") String redirectUri,\n            @Field(\"client_id\") String clientId,\n            @Field(\"client_secret\") String clientSecret,\n            @Field(\"code\") String code\n    );\n\n    @FormUrlEncoded\n    @POST(\"auth/token\")\n    Call<Token> refreshingAccessToken(\n            @Field(\"grant_type\") String grantType,\n            @Field(\"refresh_token\") String refreshToken,\n            @Field(\"client_id\") String clientId,\n            @Field(\"client_secret\") String clientSecret\n    );\n\n    @GET(\"profile\")\n    Call<Profile> getUserInfo(\n            @Header(\"authorization\") String authorization\n    );\n\n    @GET(\"collections\")\n    Call<List<Collection>> getCollections(\n            @Header(\"authorization\") String authorization\n    );\n\n    @GET(\"collections\")\n    Call<List<Collection>> editCollections(\n            @Header(\"authorization\") String authorization,\n            @Body EditCollection editCollection\n    );\n\n    @GET(\"streams/ids\")\n    Call<StreamIds> getUnreadRefs(\n            @Header(\"authorization\") String authorization,\n            @Query(\"streamId\") String streamId,\n            @Query(\"count\") int count,\n            @Query(\"unreadOnly\") boolean unreadOnly,\n            @Query(\"continuation\") String continuation\n    );\n\n    @GET(\"streams/ids\")\n    Call<StreamIds> getStarredRefs(\n            @Header(\"authorization\") String authorization,\n            @Query(\"streamId\") String streamId,\n            @Query(\"count\") int count,\n            @Query(\"continuation\") String continuation\n    );\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"entries/.mget\")\n    Call<List<Entry>> getItemContents(\n            @Header(\"authorization\") String authorization,\n            @NonNull @Body List<String> ids\n    );\n\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"feeds/.mget\")\n    Call<List<FeedItem>> getFeedsMeta(@NonNull @Body List<String> feeds);\n\n    @GET(\"feeds/{feedId}\")\n    Call<FeedItem> getFeedMeta(@Path(\"feedId\") String feedId);\n\n\n    @GET(\"streams/{feedId}/contents\")\n    Call<StreamContents> getStreamContent(\n            @Header(\"authorization\") String authorization,\n            @Path(\"feedId\") String feedId, @Query(\"count\") int count, @Query(\"continuation\") String continuation);\n\n    @GET(\"search/feeds\")\n    Call<SearchFeeds> getSearchFeeds(@Query(\"q\") String keyWord, @Query(\"n\") int count);\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"subscriptions\")\n    Call<List<EditFeed>> editFeed(\n            @Header(\"authorization\") String authorization,\n            @NonNull @Body EditFeed editFeed\n    );\n\n    // 使用retrofit进行delete请求时，发现其并不支持向服务器传body。https://www.jianshu.com/p/940fd77961db\n    //@DELETE(\"subscriptions/.mdelete\")\n    @Headers(\"Accept: application/json\")\n    @HTTP(method = \"DELETE\", path = \"subscriptions/.mdelete\", hasBody = true)\n    Call<String> delFeed(\n            @Header(\"authorization\") String authorization,\n            @Body List<String> feedIds\n    );\n\n    // 成功不返回信息\n    // 使用@body标签时不能用@FormUrlEncoded标签，不然会报以下异常\n    @Headers(\"Accept: application/json\")\n    @POST(\"markers\")\n    Call<String> markers(\n            @Header(\"authorization\") String authorization,\n            @NonNull @Body MarkerAction markerAction\n    );\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/api/FeverApi.java",
    "content": "//package me.wizos.loreadx.net.ApiService;\n//\n//import android.text.TextUtils;\n//\n//import androidx.annotation.NonNull;\n//import androidx.annotation.Nullable;\n//import androidx.collection.ArrayMap;\n//\n//import com.google.gson.GsonBuilder;\n//import com.hjq.toast.ToastUtils;\n//import com.lzy.okgo.callback.StringCallback;\n//import com.lzy.okgo.exception.HttpException;\n//import com.socks.library.KLog;\n//\n//import org.greenrobot.eventbus.EventBus;\n//import org.jsoup.Jsoup;\n//import org.jsoup.nodes.Document;\n//import org.jsoup.select.Elements;\n//\n//import java.io.IOException;\n//import java.net.ConnectException;\n//import java.net.SocketTimeoutException;\n//import java.util.ArrayList;\n//import java.util.HashSet;\n//import java.util.Iterator;\n//import java.util.List;\n//import java.util.Map;\n//\n//import me.wizos.loreadx.App;\n//import me.wizos.loreadx.R;\n//import me.wizos.loreadx.activity.ui.login.LoginResult;\n//import me.wizos.loreadx.config.GlobalConfig;\n//import me.wizos.loreadx.bean.feedly.CategoryItem;\n//import me.wizos.loreadx.bean.feedly.input.EditFeed;\n//import me.wizos.loreadx.bean.fever.BaseResponse;\n//import me.wizos.loreadx.bean.fever.Feeds;\n//import me.wizos.loreadx.bean.fever.Group;\n//import me.wizos.loreadx.bean.fever.Groups;\n//import me.wizos.loreadx.bean.fever.SavedItemIds;\n//import me.wizos.loreadx.bean.fever.UnreadItemIds;\n//import me.wizos.loreadx.bean.ttrss.request.GetHeadlines;\n//import me.wizos.loreadx.bean.ttrss.request.LoginParam;\n//import me.wizos.loreadx.bean.ttrss.request.SubscribeToFeed;\n//import me.wizos.loreadx.bean.ttrss.request.UnsubscribeFeed;\n//import me.wizos.loreadx.bean.ttrss.request.UpdateArticle;\n//import me.wizos.loreadx.bean.ttrss.result.SubscribeToFeedResult;\n//import me.wizos.loreadx.bean.ttrss.result.TTRSSArticleItem;\n//import me.wizos.loreadx.bean.ttrss.result.TTRSSResponse;\n//import me.wizos.loreadx.bean.ttrss.result.UpdateArticleResult;\n//import me.wizos.loreadx.content_extractor.Extractor;\n//import me.wizos.loreadx.db.WithDB;\n//import me.wizos.loreadx.db.Article;\n//import me.wizos.loreadx.db.Category;\n//import me.wizos.loreadx.db.Feed;\n//import me.wizos.loreadx.db.FeedCategory;\n//import me.wizos.loreadx.event.Sync;\n//import me.wizos.loreadx.net.HttpClientManager;\n//import me.wizos.loreadx.net.callback.CallbackX;\n//import me.wizos.loreadx.utils.DataUtil;\n//import me.wizos.loreadx.utils.StringUtil;\n//import me.wizos.loreadx.utils.StringUtils;\n//import okhttp3.Call;\n//import okhttp3.Callback;\n//import okhttp3.Request;\n//import retrofit2.Response;\n//import retrofit2.Retrofit;\n//import retrofit2.converter.gson.GsonConverterFactory;\n//\n///**\n// * Created by Wizos on 2019/2/8.\n// */\n//\n//public class FeverApi extends AuthApi<Feed, CategoryItem> implements LoginInterface{\n//    private FeverService service;\n//    public static String HOST = \"\";\n//    private String authorization;\n//    public int fetchContentCntForEach = 20;\n//\n//    public FeverApi() {\n//        if (TextUtils.isEmpty(HOST)) {\n//            FeverApi.HOST = App.i().getUser().getHost();\n//        }\n//        Retrofit retrofit = new Retrofit.Builder()\n//                .baseUrl(FeverApi.HOST) // 设置网络请求的Url地址, 必须以/结尾\n//                .addConverterFactory(GsonConverterFactory.create(new GsonBuilder().setLenient().create()))  // 设置数据解析器\n//                .client(HttpClientManager.i().simpleClient())\n//                .build();\n//        service = retrofit.create(FeverService.class);\n//    }\n//\n//    public static void setHOST(String HOST) {\n//        FeverApi.HOST = HOST;\n//    }\n//\n//    public void setAuthorization(String authorization) {\n//        this.authorization = authorization;\n//    }\n//\n//    public LoginResult login(String accountId, String accountPd) throws IOException {\n//        LoginParam loginParam = new LoginParam();\n//        loginParam.setUser(accountId);\n//        loginParam.setPassword(accountPd);\n//        String auth = StringUtil.MD5(accountId+\":\"+accountPd);\n//        BaseResponse loginResultTTRSSResponse = service.login(auth).execute().body();\n//        LoginResult loginResult = new LoginResult();\n//        if (loginResultTTRSSResponse!= null && loginResultTTRSSResponse.getAuth() == 1) {\n//            return loginResult.setSuccess(true).setData(auth);\n//        } else {\n//            return loginResult.setSuccess(false).setData(\"登录失败\");\n//        }\n//    }\n//\n//    public void login(String accountId, String accountPd,CallbackX cb){\n//        LoginParam loginParam = new LoginParam();\n//        loginParam.setUser(accountId);\n//        loginParam.setPassword(accountPd);\n//        String auth = StringUtil.MD5(accountId+\":\"+accountPd);\n//        service.login(auth).enqueue(new retrofit2.Callback<BaseResponse>() {\n//            @Override\n//            public void onResponse(retrofit2.Call<BaseResponse> call, Response<BaseResponse> response) {\n//                if(response.isSuccessful()){\n//                    BaseResponse loginResponse = response.body();\n//                    if( loginResponse != null && !loginResponse.isSuccessful() ){\n//                        cb.onSuccess(auth);\n//                        return;\n//                    }\n//                    cb.onFailure(\"登录失败：原因未知\" + loginResponse.toString());\n//                }else {\n//                    cb.onFailure(\"登录失败：原因未知\" + response.message());\n//                }\n//            }\n//\n//            @Override\n//            public void onFailure(retrofit2.Call<BaseResponse> call, Throwable t) {\n//                cb.onFailure(\"登录失败：\" + t.getMessage());\n//            }\n//        });\n//    }\n//\n//    public void fetchUserInfo(CallbackX cb){\n//        cb.onFailure(\"暂时不支持\");\n//    }\n//    private long syncTimeMillis;\n//\n//    @Override\n//    public void sync() {\n//        App.i().isSyncing = true;\n//        EventBus.getDefault().post(new Sync(Sync.START));\n//        try {\n//            KLog.e(\"3 - 同步订阅源信息\");\n//            EventBus.getDefault().post(new Sync(Sync.DOING, App.i().getString(R.string.main_toolbar_hint_sync_tag_feed)));\n//\n//            Groups groupsResponse = service.getCategoryItems(authorization).execute().body();\n//            if (groupsResponse == null || !groupsResponse.isSuccessful()) {\n//                throw new HttpException(\"获取失败\");\n//            }\n//\n//            Iterator<Group> categoryItemsIterator = groupsResponse.getGroups().iterator();\n//            Group group;\n//            Category category;\n//            FeedCategory feedCategoryTmp;\n//            String[] feedIds;\n//            ArrayList<Category> categories = new ArrayList<>();\n//            ArrayList<FeedCategory> feedCategories = new ArrayList<>();\n//            while (categoryItemsIterator.hasNext()) {\n//                group = categoryItemsIterator.next();\n//                if (group.getId() < 1) {\n//                    continue;\n//                }\n//                category = group.getCategry();\n//                categories.add(category);\n//\n//                feedIds = group.getFeedIds();\n//                if( null == feedIds || feedIds.length == 0 ){\n//                    continue;\n//                }\n//                for (String feedId:feedIds) {\n//                    feedCategoryTmp = new FeedCategory();\n//                    feedCategoryTmp.setCategoryId(category.getId());\n//                    feedCategoryTmp.setFeedId(feedId);\n//                    feedCategories.add(feedCategoryTmp);\n//                }\n//            }\n//\n//\n//           Feeds feedItemsTTRSSResponse = service.getFeeds(authorization).execute().body();\n//            if (!feedItemsTTRSSResponse.isSuccessful()) {\n//                throw new HttpException(\"获取失败\");\n//            }\n//\n//            Iterator<me.wizos.loreadx.bean.fever.Feed> feedItemsIterator = feedItemsTTRSSResponse.getFeeds().iterator();\n//            me.wizos.loreadx.bean.fever.Feed feedItem;\n//            ArrayList<Feed> feeds = new ArrayList<>();\n//            while (feedItemsIterator.hasNext()) {\n//                feedItem = feedItemsIterator.next();\n//                feeds.add(feedItem.convert());\n//            }\n//\n//            // 如果在获取到数据的时候就保存，那么到这里同步断了的话，可能系统内的文章就找不到响应的分组，所有放到这里保存。\n//            // 覆盖保存，只会保留最新一份。（比如在云端将文章移到的新的分组）\n//            WithDB.i().coverSaveFeeds(feeds);\n//            WithDB.i().coverSaveCategories(categories);\n//            WithDB.i().coverFeedCategory(feedCategories);\n//\n//            // updateFeedUnreadCount();\n//\n//\n//\n//            syncTimeMillis = System.currentTimeMillis();\n//            // 获取所有未读的资源\n//            EventBus.getDefault().post(new Sync(Sync.DOING, App.i().getString(R.string.sync_article_refs)));\n//            HashSet<String> idRefsSet = new HashSet<>();\n//\n//            UnreadItemIds unreadItemIdsRes = service.getUnreadItemIds(authorization).execute().body();\n//            if( null ==  unreadItemIdsRes || !unreadItemIdsRes.isSuccessful() ){\n//                throw new HttpException(\"获取文章资源失败\");\n//            }\n//            String[] unreadIds = unreadItemIdsRes.getUreadItemIds();\n//            if( null != unreadIds && unreadIds.length != 0 ){\n//                for (String id:unreadIds) {\n//                    idRefsSet.add(id);\n//                }\n//            }\n//\n//            SavedItemIds savedItemIds = service.getSavedItemIds(authorization).execute().body();\n//            if( null ==  savedItemIds || !savedItemIds.isSuccessful() ){\n//                throw new HttpException(\"获取文章资源失败\");\n//            }\n//            String[] savedIds = savedItemIds.getSavedItemIds();\n//            if( null != savedIds && savedIds.length != 0 ){\n//                for (String id:savedIds) {\n//                    idRefsSet.add(id);\n//                }\n//            }\n//\n//\n////            ids = new ArrayList<>(refsList.get(0));\n////            needFetchCount = ids.size();\n////            hadFetchCount = 0;\n//            while (idRefsSet.size() > 0){\n//                int needFetchCount = Math.min(idRefsSet.size(),fetchContentCntForEach);\n//\n//            }\n//\n//\n//            GetHeadlines getHeadlines = new GetHeadlines();\n//            getHeadlines.setSid(authorization);\n//\n//            Article article = WithDB.i().getLastArticle();\n//            if (null != article) {\n//                getHeadlines.setSince_id(article.getId());\n//            }\n//            TTRSSResponse<List<TTRSSArticleItem>> ttrssArticleItemsResponse;\n//            Iterator<TTRSSArticleItem> ttrssArticleItemIterator;\n//            ArrayList<Article> articles;\n//            TTRSSArticleItem ttrssArticleItem;\n//            int hadFetchCountUnit, hadFetchCountTotal;\n//\n//\n//            List<Integer> cloudyRefs;\n//\n//            // 获取所有未读的资源\n//            EventBus.getDefault().post(new Sync(Sync.DOING, App.i().getString(R.string.sync_article_refs)));\n//            getHeadlines.setShow_content(false);\n//            getHeadlines.setLimit(200);\n//            cloudyRefs = new ArrayList<>();\n//            hadFetchCountTotal = hadFetchCountUnit = 0;\n//            do {\n//                hadFetchCountTotal = hadFetchCountTotal + hadFetchCountUnit;\n//                getHeadlines.setSkip(hadFetchCountTotal);\n//                ttrssArticleItemsResponse = service.getHeadlines(getHeadlines).execute().body();\n//                if (!ttrssArticleItemsResponse.isSuccessful()) {\n//                    throw new HttpException(\"获取失败\");\n//                }\n//                ttrssArticleItemIterator = ttrssArticleItemsResponse.getData().iterator();\n//                hadFetchCountUnit = ttrssArticleItemsResponse.getData().size();\n//                while (ttrssArticleItemIterator.hasNext()) {\n//                    cloudyRefs.add(ttrssArticleItemIterator.next().getId());\n//                }\n//            } while (hadFetchCountUnit > 0);\n//            HashSet<String> unreadRefsSet = handleUnreadRefs(cloudyRefs);\n//\n//\n//            // 获取所有加星的资源\n//            getHeadlines.setFeed_id(\"-1\");\n//            getHeadlines.setView_mode(\"all_articles\");\n//            cloudyRefs = new ArrayList<>();\n//            hadFetchCountTotal = hadFetchCountUnit = 0;\n//            do {\n//                hadFetchCountTotal = hadFetchCountTotal + hadFetchCountUnit;\n//                getHeadlines.setSkip(hadFetchCountTotal);\n//                ttrssArticleItemsResponse = service.getHeadlines(getHeadlines).execute().body();\n//                if (!ttrssArticleItemsResponse.isSuccessful()) {\n//                    throw new HttpException(\"获取失败\");\n//                }\n//                ttrssArticleItemIterator = ttrssArticleItemsResponse.getData().iterator();\n//                hadFetchCountUnit = ttrssArticleItemsResponse.getData().size();\n//                while (ttrssArticleItemIterator.hasNext()) {\n//                    cloudyRefs.add(ttrssArticleItemIterator.next().getId());\n//                }\n//            } while (hadFetchCountUnit > 0);\n//            HashSet<String> staredRefsSet = handleStaredRefs(cloudyRefs);\n//\n////            ArrayList<HashSet<String>> refsList = splitRefs(unreadRefsSet, staredRefsSet);\n////            int readySyncArtsCapacity = refsList.get(0).size() + refsList.get(1).size() + refsList.get(2).size();\n//\n////            List<String> ids;\n////            int alreadySyncedArtsNum = 0, hadFetchCount, needFetchCount, num;\n////            ArrayList<Article> tempArticleList;\n////            ArrayMap<String, ArrayList<Article>> needReadabilityArticles = new ArrayMap<String, ArrayList<Article>>();\n////\n////            ids = new ArrayList<>(refsList.get(0));\n////            needFetchCount = ids.size();\n////            hadFetchCount = 0;\n////\n////            KLog.e(\"1 - 同步文章内容\");\n////            //   KLog.e(\"栈的数量A:\" + ids.size());\n////            syncTimeMillis = System.currentTimeMillis();\n////            while (needFetchCount > 0) {\n////                num = Math.min(needFetchCount, fetchContentCntForEach);\n////                getHeadlines.setSkip(alreadySyncedArtsNum);\n////\n////                ids.subList(hadFetchCount, hadFetchCount = hadFetchCount + num);\n////\n////                ttrssArticleItemsResponse = service.getHeadlines(getHeadlines).execute().body();\n////                if (!ttrssArticleItemsResponse.isSuccessful()) {\n////                    throw new HttpException(\"获取失败\");\n////                }\n////\n////\n////                ttrssArticleItemIterator = ttrssArticleItemsResponse.getData().iterator();\n////                hadFetchCountUnit = ttrssArticleItemsResponse.getData().size();\n////\n////                articles = new ArrayList<>(ttrssArticleItemsResponse.getData().size());\n////\n////                while (ttrssArticleItemIterator.hasNext()) {\n////                    ttrssArticleItem = ttrssArticleItemIterator.next();\n////                    articles.add(ttrssArticleItem.convert(unreadArticleChanger));\n////                }\n////                classArticles = classArticlesByFeedId(classArticles, articles);\n////                WithDB.i().saveArticles(articles);\n////\n////\n////                alreadySyncedArtsNum = alreadySyncedArtsNum + num;\n////                needFetchCount = ids.size() - hadFetchCount;\n////                EventBus.getDefault().post(new Sync(Sync.DOING, App.i().getString(R.string.main_toolbar_hint_sync_article_content, alreadySyncedArtsNum, readySyncArtsCapacity)));\n////            }\n////\n////            do {\n////                hadFetchCountTotal = hadFetchCountTotal + hadFetchCountUnit;\n////                getHeadlines.setSkip(hadFetchCountTotal);\n////                EventBus.getDefault().post(new Sync(Sync.DOING, App.i().getString(R.string.sync_article_content, hadFetchCountTotal)));\n////                ttrssArticleItemsResponse = service.getHeadlines(getHeadlines).execute().body();\n////                if (!ttrssArticleItemsResponse.isSuccessful()) {\n////                    throw new HttpException(\"获取失败\");\n////                }\n////                ttrssArticleItemIterator = ttrssArticleItemsResponse.getData().iterator();\n////                hadFetchCountUnit = ttrssArticleItemsResponse.getData().size();\n////\n////                articles = new ArrayList<>(ttrssArticleItemsResponse.getData().size());\n////\n////                while (ttrssArticleItemIterator.hasNext()) {\n////                    ttrssArticleItem = ttrssArticleItemIterator.next();\n////                    articles.add(ttrssArticleItem.convert(unreadArticleChanger));\n////                }\n////                classArticles = classArticlesByFeedId(classArticles, articles);\n////                WithDB.i().saveArticles(articles);\n////            } while (hadFetchCountUnit > 0);\n////\n////\n////\n////            ids = new ArrayList<>(refsList.get(2));\n////            needFetchCount = ids.size();\n////            hadFetchCount = 0;\n////            //            KLog.e(\"栈的数量C:\" + ids.size());\n////            while (needFetchCount > 0) {\n////                num = Math.min(needFetchCount, fetchContentCntForEach);\n////                List<Entry> entryList = service.getItemContents(ids.subList(hadFetchCount, hadFetchCount = hadFetchCount + num)).execute().body();\n////                tempArticleList = parseItemContents(entryList, new ArticleChanger() {\n////                    @Override\n////                    public Article change(Article article) {\n////                        article.setReadStatus(App.STATUS_UNREAD);\n////                        article.setStarStatus(App.STATUS_STARED);\n////                        article.setCrawlDate(syncTimeMillis);\n////                        return article;\n////                    }\n////                });\n////                WithDB.i().saveArticles(tempArticleList);\n////                needReadabilityArticles = classArticlesByFeedId(needReadabilityArticles, tempArticleList);\n////                alreadySyncedArtsNum = alreadySyncedArtsNum + num;\n////                needFetchCount = ids.size() - hadFetchCount;\n////                EventBus.getDefault().post(new Sync(Sync.DOING, App.i().getString(R.string.main_toolbar_hint_sync_article_content, alreadySyncedArtsNum, readySyncArtsCapacity)));\n////            }\n////\n////            ids = new ArrayList<>(refsList.get(1));\n////            needFetchCount = ids.size();\n////            hadFetchCount = 0;\n////            //            KLog.e(\"栈的数量B:\" + ids.size());\n////            while (needFetchCount > 0) {\n////                num = Math.min(needFetchCount, fetchContentCntForEach);\n////                List<Entry> entryList = service.getItemContents(ids.subList(hadFetchCount, hadFetchCount = hadFetchCount + num)).execute().body();\n////                tempArticleList = parseItemContents(entryList, new ArticleChanger() {\n////                    @Override\n////                    public Article change(Article article) {\n////                        article.setReadStatus(App.STATUS_READED);\n////                        article.setStarStatus(App.STATUS_STARED);\n////                        article.setCrawlDate(syncTimeMillis);\n////                        return article;\n////                    }\n////                });\n////                WithDB.i().saveArticles(tempArticleList);\n////                needReadabilityArticles = classArticlesByFeedId(needReadabilityArticles, tempArticleList);\n////                alreadySyncedArtsNum = alreadySyncedArtsNum + num;\n////                needFetchCount = ids.size() - hadFetchCount;\n////                EventBus.getDefault().post(new Sync(Sync.DOING, App.i().getString(R.string.main_toolbar_hint_sync_article_content, alreadySyncedArtsNum, readySyncArtsCapacity)));\n////            }\n////\n////            updateFeedUnreadCount();\n////\n////            WithDB.i().handleDuplicateArticle();\n////\n////            // 获取文章全文\n////            EventBus.getDefault().post(new Sync(Sync.DOING, App.i().getString(R.string.main_toolbar_hint_sync_article_readability_content)));\n////            fetchReadability(needReadabilityArticles);\n////            EventBus.getDefault().post(new Sync(Sync.END));\n//\n//            ///////////\n//\n//\n//            KLog.e(\" 2 - 同步未读文章\");\n//            ArrayMap<String, ArrayList<Article>> classArticles = new ArrayMap<String, ArrayList<Article>>();\n//\n//\n//            ArticleChanger unreadArticleChanger = new ArticleChanger() {\n//                @Override\n//                public Article change(Article article) {\n//                    article.setCrawlDate(syncTimeMillis);\n//                    return article;\n//                }\n//            };\n//            hadFetchCountUnit = 0;\n//            hadFetchCountTotal = 0;\n//            do {\n//                hadFetchCountTotal = hadFetchCountTotal + hadFetchCountUnit;\n//                getHeadlines.setSkip(hadFetchCountTotal);\n//                EventBus.getDefault().post(new Sync(Sync.DOING, App.i().getString(R.string.sync_article_content, hadFetchCountTotal)));\n//                ttrssArticleItemsResponse = service.getHeadlines(getHeadlines).execute().body();\n//                if (!ttrssArticleItemsResponse.isSuccessful()) {\n//                    throw new HttpException(\"获取失败\");\n//                }\n//                ttrssArticleItemIterator = ttrssArticleItemsResponse.getData().iterator();\n//                hadFetchCountUnit = ttrssArticleItemsResponse.getData().size();\n//\n//                articles = new ArrayList<>(ttrssArticleItemsResponse.getData().size());\n//\n//                while (ttrssArticleItemIterator.hasNext()) {\n//                    ttrssArticleItem = ttrssArticleItemIterator.next();\n//                    articles.add(ttrssArticleItem.convert(unreadArticleChanger));\n//                }\n//                classArticles = classArticlesByFeedId(classArticles, articles);\n//                WithDB.i().saveArticles(articles);\n//            } while (hadFetchCountUnit > 0);\n//\n//\n//            getHeadlines.setFeed_id(\"-1\");\n//            getHeadlines.setView_mode(\"all_articles\");\n//            ArticleChanger staredArticleChanger = new ArticleChanger() {\n//                @Override\n//                public Article change(Article article) {\n//                    article.setStarStatus(App.STATUS_STARED);\n//                    article.setCrawlDate(syncTimeMillis);\n//                    return article;\n//                }\n//            };\n//            int fetchCountTotal = hadFetchCountTotal;\n//            KLog.e(\" 1 - 同步加星文章\");\n//            hadFetchCountUnit = 0;\n//            hadFetchCountTotal = 0;\n//            do {\n//                hadFetchCountTotal = hadFetchCountTotal + hadFetchCountUnit;\n//                getHeadlines.setSkip(hadFetchCountTotal);\n//                EventBus.getDefault().post(new Sync(Sync.DOING, App.i().getString(R.string.sync_article_content, fetchCountTotal + hadFetchCountTotal)));\n//                ttrssArticleItemsResponse = service.getHeadlines(getHeadlines).execute().body();\n//                if (!ttrssArticleItemsResponse.isSuccessful()) {\n//                    throw new HttpException(\"获取失败\");\n//                }\n//                ttrssArticleItemIterator = ttrssArticleItemsResponse.getData().iterator();\n//                hadFetchCountUnit = ttrssArticleItemsResponse.getData().size();\n//\n//                articles = new ArrayList<>(ttrssArticleItemsResponse.getData().size());\n//\n//                while (ttrssArticleItemIterator.hasNext()) {\n//                    ttrssArticleItem = ttrssArticleItemIterator.next();\n//                    articles.add(ttrssArticleItem.convert(staredArticleChanger));\n//                }\n//                classArticles = classArticlesByFeedId(classArticles, articles);\n//                WithDB.i().saveArticles(articles);\n//            } while (hadFetchCountUnit > 0);\n//\n//            updateFeedUnreadCount();\n//\n//            WithDB.i().handleDuplicateArticle();\n//\n//            // 获取文章全文\n//            EventBus.getDefault().post(new Sync(Sync.DOING, App.i().getString(R.string.main_toolbar_hint_sync_article_readability_content)));\n//            fetchReadability(classArticles);\n//            EventBus.getDefault().post(new Sync(Sync.END));\n//        } catch (HttpException e) {\n//            KLog.e(\"同步时产生HttpException：\" + e.message());\n//            e.printStackTrace();\n//            handleException(e);\n//        } catch (ConnectException e) {\n//            KLog.e(\"同步时产生异常ConnectException\");\n//            e.printStackTrace();\n//            handleException(e);\n//        } catch (SocketTimeoutException e) {\n//            KLog.e(\"同步时产生异常SocketTimeoutException\");\n//            e.printStackTrace();\n//            handleException(e);\n//        } catch (IOException e) {\n//            KLog.e(\"同步时产生异常IOException\");\n//            e.printStackTrace();\n//            handleException(e);\n//        } catch (RuntimeException e) {\n//            KLog.e(\"同步时产生异常RuntimeException\");\n//            e.printStackTrace();\n//            handleException(e);\n//        }\n//        App.i().isSyncing = false;\n//    }\n//\n//    private void handleException(Exception e) {\n//        if (e instanceof HttpException) {\n//            ToastUtils.show(\"网络异常：\" + e.getMessage());\n//            // EventBus.getDefault().post(new Sync(Sync.NEED_AUTH));\n//        } else {\n//            updateFeedUnreadCount();\n//        }\n//\n//        App.i().isSyncing = false;\n//        EventBus.getDefault().post(new Sync(Sync.ERROR));\n//    }\n//\n//    @Override\n//    public void renameTag(String tagId, String targetName, CallbackX cb) {\n//        cb.onFailure(\"暂时不支持\");\n//    }\n//\n//    public void addFeed(EditFeed editFeed, CallbackX cb) {\n//        SubscribeToFeed subscribeToFeed = new SubscribeToFeed();\n//        subscribeToFeed.setSid(authorization);\n//        subscribeToFeed.setFeed_url(editFeed.getId().replace(\"feed/\", \"\"));\n//        if (editFeed.getCategoryItems() != null && editFeed.getCategoryItems().size() != 0) {\n//            subscribeToFeed.setCategory_id(editFeed.getCategoryItems().get(0).getId());\n//        }\n//        service.subscribeToFeed(subscribeToFeed).enqueue(new retrofit2.Callback<TTRSSResponse<SubscribeToFeedResult>>() {\n//            @Override\n//            public void onResponse(retrofit2.Call<TTRSSResponse<SubscribeToFeedResult>> call, Response<TTRSSResponse<SubscribeToFeedResult>> response) {\n//                if (response.isSuccessful() && response.body().isSuccessful()) {\n//                    KLog.e(\"添加成功\" + response.body().toString());\n//                    cb.onSuccess(\"添加成功\");\n//                } else {\n//                    cb.onFailure(\"响应失败\");\n//                }\n//            }\n//\n//            @Override\n//            public void onFailure(retrofit2.Call<TTRSSResponse<SubscribeToFeedResult>> call, Throwable t) {\n//                cb.onFailure(\"添加失败\");\n//                KLog.e(\"添加失败\");\n//            }\n//        });\n//    }\n//\n////    public CallWrap addFeed(EditFeed editFeed) {\n////        SubscribeToFeed subscribeToFeed = new SubscribeToFeed();\n////        subscribeToFeed.setSid(authorization);\n////        subscribeToFeed.setFeed_url(editFeed.getId().replace(\"feed/\", \"\"));\n////        if (editFeed.getCategoryItems() != null && editFeed.getCategoryItems().size() != 0) {\n////            subscribeToFeed.setCategory_id(editFeed.getCategoryItems().get(0).getId());\n////        }\n////\n////        return new CallWrap<>(service.subscribeToFeed(subscribeToFeed), new CallbackWarp<TTRSSResponse<SubscribeToFeedResult>>() {\n////            @Override\n////            public void onResponse(retrofit2.Call<TTRSSResponse<SubscribeToFeedResult>> call, retrofit2.Response<TTRSSResponse<SubscribeToFeedResult>> response, retrofit2.Callback callbackResult) {\n////                if (response.isSuccessful() && response.body().isSuccessful()) {\n////                    KLog.e(\"添加成功\" + response.body().toString());\n////                    callbackResult.onResponse(call, response);\n////                } else {\n////                    callbackResult.onFailure(call, new RuntimeException(\"响应失败\"));\n////                }\n////            }\n////\n////            @Override\n////            public void onFailure(retrofit2.Call<TTRSSResponse<SubscribeToFeedResult>> call, Throwable t, retrofit2.Callback callbackResult) {\n////                callbackResult.onFailure(call, t);\n////                KLog.e(\"添加失败\");\n////            }\n////        });\n////    }\n//\n//    @Override\n//    public void renameFeed(String feedId, String renamedTitle, CallbackX cb) {\n//        cb.onFailure(\"暂时不支持\");\n//    }\n//\n//    /**\n//     * 订阅，编辑feed\n//     *\n//     * @param feedId\n//     * @param feedTitle\n//     * @param categoryItems\n//     * @param cb\n//     */\n//    public void editFeed(@NonNull String feedId, @Nullable String feedTitle, @Nullable ArrayList<CategoryItem> categoryItems, StringCallback cb) {\n//    }\n//\n//\n//    @Override\n//    public void editFeedCategories(List<CategoryItem> lastCategoryItems, EditFeed editFeed,CallbackX cb) {\n//        cb.onFailure(\"暂时不支持\");\n//    }\n//\n//    public void unsubscribeFeed(String feedId,CallbackX cb) {\n//        UnsubscribeFeed unsubscribeFeed = new UnsubscribeFeed();\n//        unsubscribeFeed.setSid(authorization);\n//        unsubscribeFeed.setFeed_id(Integer.valueOf(feedId));\n//        service.unsubscribeFeed(unsubscribeFeed).enqueue(new retrofit2.Callback<TTRSSResponse<Map>>() {\n//            @Override\n//            public void onResponse(retrofit2.Call<TTRSSResponse<Map>> call, Response<TTRSSResponse<Map>> response) {\n//                if(response.isSuccessful() && \"OK\".equals(response.body().getData().get(\"status\"))){\n//                    cb.onSuccess(\"退订成功\");\n//                }else {\n//                    cb.onFailure(\"退订失败\" + response.body().getData().toString());\n//                }\n//            }\n//\n//            @Override\n//            public void onFailure(retrofit2.Call<TTRSSResponse<Map>> call, Throwable t) {\n//                cb.onSuccess(\"退订失败\" + t.getMessage());\n//            }\n//        });\n//    }\n//\n//\n//    private void markArticles(int field, int mode, List<String> ids,CallbackX cb) {\n//        UpdateArticle updateArticle = new UpdateArticle();\n//        updateArticle.setSid(authorization);\n//        updateArticle.setArticle_ids(StringUtils.join(\",\", ids));\n//        updateArticle.setField(field);\n//        updateArticle.setMode(mode);\n//        service.updateArticle(updateArticle).enqueue(new retrofit2.Callback<TTRSSResponse<UpdateArticleResult>>() {\n//            @Override\n//            public void onResponse(retrofit2.Call<TTRSSResponse<UpdateArticleResult>> call, Response<TTRSSResponse<UpdateArticleResult>> response) {\n//                if (response.isSuccessful() ){\n//                    cb.onSuccess(null);\n//                }else {\n//                    cb.onFailure(\"修改失败，原因未知\");\n//                }\n//            }\n//\n//            @Override\n//            public void onFailure(retrofit2.Call<TTRSSResponse<UpdateArticleResult>> call, Throwable t) {\n//                cb.onFailure(\"修改失败，原因未知\");\n//            }\n//        });\n//    }\n//\n//    private void markArticle(int field, int mode, String articleId,CallbackX cb) {\n//        UpdateArticle updateArticle = new UpdateArticle();\n//        updateArticle.setSid(authorization);\n//        updateArticle.setArticle_ids(articleId);\n//        updateArticle.setField(field);\n//        updateArticle.setMode(mode);\n//        service.updateArticle(updateArticle).enqueue(new retrofit2.Callback<TTRSSResponse<UpdateArticleResult>>() {\n//            @Override\n//            public void onResponse(retrofit2.Call<TTRSSResponse<UpdateArticleResult>> call, Response<TTRSSResponse<UpdateArticleResult>> response) {\n//                if (response.isSuccessful() ){\n//                    cb.onSuccess(null);\n//                }else {\n//                    cb.onFailure(\"修改失败，原因未知\");\n//                }\n//            }\n//\n//            @Override\n//            public void onFailure(retrofit2.Call<TTRSSResponse<UpdateArticleResult>> call, Throwable t) {\n//                cb.onFailure(\"修改失败，原因未知\");\n//            }\n//        });\n//    }\n//\n//    public void markArticleListReaded(List<String> articleIds,CallbackX cb) {\n//        markArticles(2, 0, articleIds, cb);\n//    }\n//\n//    public void markArticleReaded(String articleId, CallbackX cb) {\n//        markArticle(2, 0, articleId, cb);\n//    }\n//\n//    public void markArticleUnread(String articleId, CallbackX cb) {\n//        markArticle(2, 1, articleId, cb);\n//    }\n//\n//    public void markArticleStared(String articleId, CallbackX cb) {\n//        markArticle(0, 1, articleId, cb);\n//    }\n//\n//    public void markArticleUnstar(String articleId,CallbackX cb) {\n//        markArticle(0, 0, articleId, cb);\n//    }\n//\n////\n////    private retrofit2.Call<TTRSSResponse<UpdateArticleResult>> markArticles(int field, int mode, List<String> ids) {\n////        UpdateArticle updateArticle = new UpdateArticle();\n////        updateArticle.setSid(authorization);\n////        updateArticle.setArticle_ids(StringUtils.join(\",\", ids));\n////        updateArticle.setField(field);\n////        updateArticle.setMode(mode);\n////        return service.updateArticle(updateArticle);\n////    }\n////\n////    private retrofit2.Call<TTRSSResponse<UpdateArticleResult>> markArticle(int field, int mode, String articleId) {\n////        UpdateArticle updateArticle = new UpdateArticle();\n////        updateArticle.setSid(authorization);\n////        updateArticle.setArticle_ids(articleId);\n////        updateArticle.setField(field);\n////        updateArticle.setMode(mode);\n////        return service.updateArticle(updateArticle);\n////    }\n////\n////    public retrofit2.Call markArticleListReaded(List<String> articleIds) {\n////        return markArticles(2, 0, articleIds);\n////    }\n////\n////    public CallWrap markArticleReaded(String articleId) {\n////        return new CallWrap<>(markArticle(2, 0, articleId), new CallbackWarp() {\n////            @Override\n////            public void onResponse(retrofit2.Call call, retrofit2.Response response, retrofit2.Callback callbackResult) {\n////                if (response.isSuccessful()) {\n////                    callbackResult.onResponse(call, response);\n////                } else {\n////                    callbackResult.onFailure(call, new RuntimeException(\"响应失败\"));\n////                }\n////            }\n////\n////            @Override\n////            public void onFailure(retrofit2.Call call, Throwable t, retrofit2.Callback callbackResult) {\n////                callbackResult.onFailure(call, t);\n////            }\n////        });\n////    }\n////\n////    public CallWrap markArticleUnread(String articleId) {\n////        return new CallWrap(markArticle(2, 1, articleId), new CallbackWarp() {\n////            @Override\n////            public void onResponse(retrofit2.Call call, retrofit2.Response response, retrofit2.Callback callbackResult) {\n////                if (response.isSuccessful()) {\n////                    callbackResult.onResponse(call, response);\n////                } else {\n////                    callbackResult.onFailure(call, new RuntimeException(\"响应失败\"));\n////                }\n////            }\n////\n////            @Override\n////            public void onFailure(retrofit2.Call call, Throwable t, retrofit2.Callback callbackResult) {\n////                callbackResult.onFailure(call, t);\n////            }\n////        });\n////    }\n////\n////    public CallWrap markArticleStared(String articleId) {\n////        return new CallWrap(markArticle(0, 1, articleId), new CallbackWarp() {\n////            @Override\n////            public void onResponse(retrofit2.Call call, retrofit2.Response response, retrofit2.Callback callbackResult) {\n////                if (response.isSuccessful()) {\n////                    callbackResult.onResponse(call, response);\n////                } else {\n////                    callbackResult.onFailure(call, new RuntimeException(\"响应失败\"));\n////                }\n////            }\n////\n////            @Override\n////            public void onFailure(retrofit2.Call call, Throwable t, retrofit2.Callback callbackResult) {\n////                callbackResult.onFailure(call, t);\n////            }\n////        });\n////    }\n////\n////    public CallWrap markArticleUnstar(String articleId) {\n////        return new CallWrap(markArticle(0, 0, articleId), new CallbackWarp() {\n////            @Override\n////            public void onResponse(retrofit2.Call call, retrofit2.Response response, retrofit2.Callback callbackResult) {\n////                if (response.isSuccessful()) {\n////                    callbackResult.onResponse(call, response);\n////                } else {\n////                    callbackResult.onFailure(call, new RuntimeException(\"响应失败\"));\n////                }\n////            }\n////\n////            @Override\n////            public void onFailure(retrofit2.Call call, Throwable t, retrofit2.Callback callbackResult) {\n////                callbackResult.onFailure(call, t);\n////            }\n////        });\n////    }\n//\n//    private HashSet<String> handleUnreadRefs(List<Integer> ids) {\n//        List<Article> localUnreadArticles = WithDB.i().getArtsUnreadNoOrder();\n//        Map<String, Article> localUnreadArticlesMap = new ArrayMap<>(localUnreadArticles.size());\n//        List<Article> changedArticles = new ArrayList<>();\n//        // 筛选下来，最终要去云端获取内容的未读Refs的集合\n//        HashSet<String> tempUnreadIds = new HashSet<>(ids.size());\n//        // 数据量大的一方\n//        for (Article article : localUnreadArticles) {\n//            localUnreadArticlesMap.put(article.getId(), article);\n//        }\n//        // 数据量小的一方\n//        Article article;\n//        for (Integer articleId : ids) {\n//            article = localUnreadArticlesMap.get(articleId+\"\");\n//            if (article != null) {\n//                localUnreadArticlesMap.remove(articleId+\"\");\n//            } else {\n//                article = WithDB.i().getArticle(articleId+\"\");\n//                if (article != null && article.getReadStatus() == App.STATUS_READED) {\n//                    article.setReadStatus(App.STATUS_UNREAD);\n//                    changedArticles.add(article);\n//                } else {\n//                    // 本地无，而云端有，加入要请求的未读资源\n//                    tempUnreadIds.add(articleId+\"\");\n//                }\n//            }\n//        }\n//        for (Map.Entry<String, Article> entry : localUnreadArticlesMap.entrySet()) {\n//            if (entry.getKey() != null) {\n//                article = localUnreadArticlesMap.get(entry.getKey());\n//                // 本地未读设为已读\n//                article.setReadStatus(App.STATUS_READED);\n//                changedArticles.add(article);\n//            }\n//        }\n//\n//        WithDB.i().saveArticles(changedArticles);\n//        return tempUnreadIds;\n//    }\n//\n//    private HashSet<String> handleStaredRefs(List<Integer> streamIds) {\n//        List<Article> localStarredArticles = WithDB.i().getArtsStared();\n//        Map<String, Article> localStarredArticlesMap = new ArrayMap<>(localStarredArticles.size());\n//        List<Article> changedArticles = new ArrayList<>();\n//        HashSet<String> tempStarredIds = new HashSet<>(streamIds.size());\n//\n//        // 第1步，遍历数据量大的一方A，将其比对项目放入Map中\n//        for (Article article : localStarredArticles) {\n//            localStarredArticlesMap.put(article.getId(), article);\n//        }\n//\n//        // 第2步，遍历数据量小的一方B。到Map中找，是否含有b中的比对项。有则XX，无则YY\n//        Article article;\n//        for (Integer articleId : streamIds) {\n//            article = localStarredArticlesMap.get(articleId+\"\");\n//            if (article != null) {\n//                localStarredArticlesMap.remove(articleId+\"\");\n//            } else {\n//                article = WithDB.i().getArticle(articleId+\"\");\n//                if (article != null) {\n//                    article.setStarStatus(App.STATUS_STARED);\n//                    changedArticles.add(article);\n//                } else {\n//                    // 本地无，而云远端有，加入要请求的未读资源\n//                    tempStarredIds.add(articleId+\"\");\n//                }\n//            }\n//        }\n//\n//        for (Map.Entry<String, Article> entry : localStarredArticlesMap.entrySet()) {\n//            if (entry.getKey() != null) {\n//                article = localStarredArticlesMap.get(entry.getKey());\n//                article.setStarStatus(App.STATUS_UNSTAR);\n//                changedArticles.add(article);// 取消加星\n//            }\n//        }\n//\n//        WithDB.i().saveArticles(changedArticles);\n//        return tempStarredIds;\n//    }\n//\n//    /**\n//     * 将 未读资源 和 加星资源，去重分为3组\n//     *\n//     * @param tempUnreadIds\n//     * @param tempStarredIds\n//     * @return\n//     */\n//    private ArrayList<HashSet<String>> splitRefs(HashSet<String> tempUnreadIds, HashSet<String> tempStarredIds) {\n////        KLog.e(\"【reRefs1】云端未读\" + tempUnreadIds.size() + \"，云端加星\" + tempStarredIds.size());\n//        int total = tempUnreadIds.size() > tempStarredIds.size() ? tempStarredIds.size() : tempUnreadIds.size();\n//\n//        HashSet<String> reUnreadUnstarRefs;\n//        HashSet<String> reReadStarredRefs = new HashSet<>(tempStarredIds.size());\n//        HashSet<String> reUnreadStarredRefs = new HashSet<>(total);\n//\n//        for (String id : tempStarredIds) {\n//            if (tempUnreadIds.contains(id)) {\n//                tempUnreadIds.remove(id);\n//                reUnreadStarredRefs.add(id);\n//            } else {\n//                reReadStarredRefs.add(id);\n//            }\n//        }\n//        reUnreadUnstarRefs = tempUnreadIds;\n//\n//        ArrayList<HashSet<String>> refsList = new ArrayList<>();\n//        refsList.add(reUnreadUnstarRefs);\n//        refsList.add(reReadStarredRefs);\n//        refsList.add(reUnreadStarredRefs);\n////        KLog.e(\"【reRefs2】\" + reUnreadUnstarRefs.size() + \"--\" + reReadStarredRefs.size() + \"--\" + reUnreadStarredRefs.size());\n//        return refsList;\n//    }\n//\n//    private ArrayMap<String, ArrayList<Article>> classArticlesByFeedId(ArrayMap<String, ArrayList<Article>> feedIds, ArrayList<Article> articles) {\n//        if (null == feedIds) {\n//            return new ArrayMap<String, ArrayList<Article>>();\n//        }\n//        if (articles != null) {\n//            ArrayList<Article> arrayList;\n//            for (Article article : articles) {\n//                arrayList = feedIds.get(article.getFeedId());\n//                if (null == arrayList) {\n//                    arrayList = new ArrayList<Article>();\n//                    arrayList.add(article);\n//                    feedIds.put(article.getFeedId(), arrayList);\n//                } else {\n//                    arrayList.add(article);\n//                }\n//            }\n//        }\n//        return feedIds;\n//    }\n//\n//    private void fetchReadability(ArrayMap<String, ArrayList<Article>> feedIds) {\n//        if (null == feedIds) {\n//            return;\n//        }\n//        KLog.e(\"易读，获取到的订阅源：\" + feedIds.size());\n//\n//        ArrayMap<String, ArrayList<Article>> needReadabilityFeedIds = new ArrayMap<String, ArrayList<Article>>();\n//        for (Map.Entry<String, ArrayList<Article>> entry : feedIds.entrySet()) {\n//            //KLog.e(\"需要获取易读，Key：\" + entry.getKey() + \" , \"  +  GlobalConfig.i().getDisplayMode(entry.getKey()) );\n//            if (App.DISPLAY_READABILITY.equals(GlobalConfig.i().getDisplayMode(entry.getKey()))) {\n//                needReadabilityFeedIds.put(entry.getKey(), entry.getValue());\n//            }\n//        }\n//\n//        KLog.e(\"易读，需要获取的数量：\" + needReadabilityFeedIds.entrySet().size());\n//        for (Map.Entry<String, ArrayList<Article>> entry : needReadabilityFeedIds.entrySet()) {\n//            for (final Article article : entry.getValue()) {\n//                //KLog.e(\"====获取：\" + entry.getKey() + \" , \" + article.getTitle() + \" , \" + article.getLink());\n//                if (TextUtils.isEmpty(article.getLink())) {\n//                    return;\n//                }\n//                //KLog.e(\"====开始请求\" );\n//                Request request = new Request.Builder().url(article.getLink()).build();\n//                Call call = HttpClientManager.i().simpleClient().newCall(request);\n//                call.enqueue(new Callback() {\n//                    @Override\n//                    public void onFailure(Call call, IOException e) {\n//                        KLog.e(\"获取失败\");\n//                    }\n//\n//                    @Override\n//                    public void onResponse(Call call, okhttp3.Response response) throws IOException {\n//                        if (!response.isSuccessful()) {\n//                            return;\n//                        }\n//                        //KLog.e(\"已保存易读\");\n//                        Document doc = Jsoup.parse(response.body().byteStream(), DataUtil.getCharsetFromContentType(response.body().contentType().toString()), article.getLink());\n//                        String content = Extractor.getData(article.getLink(), doc);\n//                        if (TextUtils.isEmpty(content)) {\n//                            return;\n//                        }\n//                        content = StringUtil.getOptimizedContent(article.getLink(), content);\n//                        //KLog.e(\"易读：\" + content );\n//                        article.setData(content);\n//                        String summary = StringUtil.getOptimizedSummary(content);\n//                        article.setSummary(summary);\n//\n//                        // 获取第1个图片作为封面\n//                        Elements elements = Jsoup.parseBodyFragment(content).getElementsByTag(\"img\");\n//                        if (elements.size() > 0) {\n//                            String src = elements.get(0).attr(\"abs:src\");\n//                            // article.setEnclosure(src);\n//                            article.setImage(src);\n//                        }\n//                        WithDB.i().updateArticle(article);\n//                    }\n//                });\n//            }\n//        }\n//    }\n//\n//\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/api/FeverService.java",
    "content": "//package me.wizos.loreadx.net.ApiService;\n//\n//import androidx.annotation.NonNull;\n//\n//import java.util.List;\n//import java.util.Map;\n//\n//import me.wizos.loreadx.bean.fever.BaseResponse;\n//import me.wizos.loreadx.bean.fever.Feeds;\n//import me.wizos.loreadx.bean.fever.Groups;\n//import me.wizos.loreadx.bean.fever.Items;\n//import me.wizos.loreadx.bean.fever.SavedItemIds;\n//import me.wizos.loreadx.bean.fever.UnreadItemIds;\n//import me.wizos.loreadx.bean.ttrss.request.GetFeeds;\n//import me.wizos.loreadx.bean.ttrss.request.GetHeadlines;\n//import me.wizos.loreadx.bean.ttrss.request.SubscribeToFeed;\n//import me.wizos.loreadx.bean.ttrss.request.UnsubscribeFeed;\n//import me.wizos.loreadx.bean.ttrss.request.UpdateArticle;\n//import me.wizos.loreadx.bean.ttrss.result.SubscribeToFeedResult;\n//import me.wizos.loreadx.bean.ttrss.result.TTRSSArticleItem;\n//import me.wizos.loreadx.bean.ttrss.result.TTRSSFeedItem;\n//import me.wizos.loreadx.bean.ttrss.result.TTRSSResponse;\n//import me.wizos.loreadx.bean.ttrss.result.UpdateArticleResult;\n//import retrofit2.Call;\n//import retrofit2.http.Body;\n//import retrofit2.http.Field;\n//import retrofit2.http.FormUrlEncoded;\n//import retrofit2.http.Headers;\n//import retrofit2.http.POST;\n//import retrofit2.http.Query;\n//\n///**\n// * Created by Wizos on 2019/11/23.\n// */\n//\n//public interface FeverService {\n//    // Post请求的文本参数则用注解@Field来声明，同时还必须给方法添加注解@FormUrlEncoded来告知Retrofit参数为表单参数，如果只为参数增加@Field注解，而不给方法添加@FormUrlEncoded注解运行时会抛异常。\n//    @FormUrlEncoded\n//    @POST(\"?api\")\n//    Call<BaseResponse> login(\n//            @NonNull @Query(\"api_key\") String apiKey\n//    );\n//    @FormUrlEncoded\n//    @POST(\"?api&groups\")\n//    Call<Groups> getCategoryItems(\n//            //@NonNull @Field(\"api_key\") String apiKey\n//            @NonNull @Query(\"api_key\") String apiKey\n//    );\n//    @FormUrlEncoded\n//    @POST(\"?api&feeds\")\n//    Call<Feeds> getFeeds(\n//            @NonNull @Query(\"api_key\") String apiKey\n//    );\n//\n//    @FormUrlEncoded\n//    @POST(\"?api&unread_item_ids\")\n//    Call<UnreadItemIds> getUnreadItemIds(\n//            @NonNull @Query(\"api_key\") String apiKey\n//    );\n//\n//    @FormUrlEncoded\n//    @POST(\"?api&saved_item_ids\")\n//    Call<SavedItemIds> getSavedItemIds(\n//            @NonNull @Query(\"api_key\") String apiKey\n//    );\n//\n//\n//    /**\n//     * 每次最多 50 条\n//     */\n//    @FormUrlEncoded\n//    @POST(\"?api&items\")\n//    Call<Items> getItems(\n//            @NonNull @Query(\"api_key\") String apiKey,\n//            @Query(\"since_id\") String sinceId,\n//            @Query(\"max_id\") String maxId,\n//            @Query(\"with_ids\") String withIds\n//    );\n//\n//\n//\n//\n//\n//    @FormUrlEncoded\n//    @POST(\"api/\")\n//    Call<TTRSSResponse<UpdateArticleResult>> updateArticle(\n//            @NonNull @Body UpdateArticle updateArticle\n//    );\n//\n//\n//    @Headers(\"Accept: application/json\")\n//    @POST(\"api/\")\n//    Call<TTRSSResponse<SubscribeToFeedResult>> subscribeToFeed(\n//            @NonNull @Body SubscribeToFeed subscribeToFeed\n//    );\n//\n//    @Headers(\"Accept: application/json\")\n//    @POST(\"api/\")\n//    Call<TTRSSResponse<Map>> unsubscribeFeed(\n//            @NonNull @Body UnsubscribeFeed unsubscribeFeed\n//    );\n//\n////    @Headers(\"Accept: application/json\")\n////    @POST(\"subscriptions\")\n////    Call<List<EditFeed>> editFeed(\n////            @NonNull @Body EditFeed editFeed\n////    );\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/api/InoReaderApi.java",
    "content": "package me.wizos.loread.network.api;\n\nimport android.text.TextUtils;\n\nimport androidx.annotation.NonNull;\nimport androidx.collection.ArrayMap;\n\nimport com.hjq.toast.ToastUtils;\nimport com.jeremyliao.liveeventbus.LiveEventBus;\nimport com.lzy.okgo.exception.HttpException;\nimport com.socks.library.KLog;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.bean.Token;\nimport me.wizos.loread.bean.feedly.CategoryItem;\nimport me.wizos.loread.bean.feedly.input.EditFeed;\nimport me.wizos.loread.bean.inoreader.ItemIds;\nimport me.wizos.loread.bean.inoreader.ItemRefs;\nimport me.wizos.loread.bean.inoreader.LoginResult;\nimport me.wizos.loread.bean.inoreader.SubCategories;\nimport me.wizos.loread.bean.inoreader.Subscription;\nimport me.wizos.loread.bean.inoreader.UserInfo;\nimport me.wizos.loread.bean.inoreader.itemContents.Item;\nimport me.wizos.loread.config.ArticleActionConfig;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.db.Category;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.Feed;\nimport me.wizos.loread.db.FeedCategory;\nimport me.wizos.loread.db.User;\nimport me.wizos.loread.network.HttpClientManager;\nimport me.wizos.loread.network.StringConverterFactory;\nimport me.wizos.loread.network.SyncWorker;\nimport me.wizos.loread.network.callback.CallbackX;\nimport okhttp3.FormBody;\nimport okhttp3.RequestBody;\nimport retrofit2.Call;\nimport retrofit2.Callback;\nimport retrofit2.Response;\nimport retrofit2.Retrofit;\nimport retrofit2.converter.gson.GsonConverterFactory;\n\nimport static me.wizos.loread.utils.StringUtils.getString;\n\n//import okhttp3.Call;\n\n/**\n * 本接口对接 InoReader 服务，从他那获取数据\n * implements LoginInterface\n * @author Wizos on 2019/2/15.\n */\n\npublic class InoReaderApi extends OAuthApi<Feed, CategoryItem> implements LoginInterface{\n    public static final String APP_ID = \"1000001277\";\n    public static final String APP_KEY = \"8dByWzO4AYi425yx5glICKntEY2g3uJo\";\n    private static String OFFICIAL_BASE_URL = \"https://www.inoreader.com\";\n    private static final String REDIRECT_URI = \"loread://oauth_inoreader\";\n\n//    public static final String CLIENTLOGIN = \"/accounts/ClientLogin\";\n//    public static final String USER_INFO = \"/reader/api/0/user-info\";\n//    public static final String ITEM_IDS = \"/reader/api/0/stream/items/ids\"; // 获取所有文章的id\n//    public static final String ITEM_CONTENTS = \"/reader/api/0/stream/items/contents\"; // 获取流的内容\n//    public static final String EDIT_TAG = \"/reader/api/0/edit-tag\";\n//    public static final String RENAME_TAG = \"/reader/api/0/rename-tag\";\n//    public static final String EDIT_FEED = \"/reader/api/0/subscription/edit\";\n//    public static final String ADD_FEED = \"/reader/api/0/subscription/quickadd\";\n//    public static final String SUSCRIPTION_LIST = \"/reader/api/0/subscription/list\"; // 这个不知道现在用在了什么地方\n//    public static final String TAG_LIST = \"/reader/api/0/tag/list\";\n//    public static final String STREAM_PREFS = \"/reader/api/0/preference/stream/list\";\n//    public static final String UNREAD_COUNTS = \"/reader/api/0/unread-count\";\n//    public static final String STREAM_CONTENTS = \"/reader/api/0/stream/contents/\";\n//    public static final String Stream_Contents_Atom = \"/reader/atom\";\n//    public static final String Stream_Contents_User = \"/reader/api/0/stream/contents/user/\";\n    // 系统默认的分类\n//    public static final String READING_LIST = \"/state/com.google/reading-list\";\n//    public static final String NO_LABEL = \"/state/com.google/no-label\";\n//    public static final String STARRED = \"/state/com.google/starred\";\n//    public static final String UNREAND = \"/state/com.google/unread\";\n\n    /*\n    Code \tDescription\n    200 \tRequest OK\n    400 \tMandatory parameter(s) missing\n    401 \tEnd-user not authorized\n    403 \tYou are not sending the correct AppID and/or AppSecret\n    404 \tMethod not implemented\n    429 \tDaily limit reached for this zone\n    503 \tService unavailable\n     */\n\n    private InoReaderService service;\n\n    public InoReaderApi() {\n        if(App.i().getUser() != null){\n            InoReaderApi.OFFICIAL_BASE_URL = App.i().getUser().getHost();\n        }\n\n        if (!InoReaderApi.OFFICIAL_BASE_URL.endsWith(\"/\")) {\n            InoReaderApi.OFFICIAL_BASE_URL = InoReaderApi.OFFICIAL_BASE_URL + \"/\";\n        }\n\n        Retrofit retrofit = new Retrofit.Builder()\n                .baseUrl(InoReaderApi.OFFICIAL_BASE_URL) // 设置网络请求的Url地址, 必须以/结尾\n                .addConverterFactory(StringConverterFactory.create())\n                .addConverterFactory(GsonConverterFactory.create())  // 设置数据解析器\n                .client(HttpClientManager.i().inoreaderHttpClient())\n                .build();\n        service = retrofit.create(InoReaderService.class);\n    }\n\n    public InoReaderApi(String host) {\n        if (!TextUtils.isEmpty(host)) {\n            InoReaderApi.OFFICIAL_BASE_URL = host;\n        }\n\n        if (!InoReaderApi.OFFICIAL_BASE_URL.endsWith(\"/\")) {\n            InoReaderApi.OFFICIAL_BASE_URL = InoReaderApi.OFFICIAL_BASE_URL + \"/\";\n        }\n        Retrofit retrofit = new Retrofit.Builder()\n                .baseUrl(InoReaderApi.OFFICIAL_BASE_URL) // 设置网络请求的Url地址, 必须以/结尾\n                .addConverterFactory(StringConverterFactory.create())\n                .addConverterFactory(GsonConverterFactory.create())  // 设置数据解析器\n                .client(HttpClientManager.i().inoreaderHttpClient())\n                .build();\n        service = retrofit.create(InoReaderService.class);\n    }\n\n    public static void setHost(String host) {\n        InoReaderApi.OFFICIAL_BASE_URL = host;\n        KLog.i(\"HOST 地址：\" + OFFICIAL_BASE_URL );\n    }\n\n    public void login(String accountId, String accountPd,CallbackX cb){\n        service.login(accountId, accountPd).enqueue(new retrofit2.Callback<String>() {\n            @Override\n            public void onResponse(retrofit2.Call<String> call, Response<String> response) {\n                if(response.isSuccessful()){\n                    String result = response.body();\n                    LoginResult loginResult = new LoginResult(result);\n                    KLog.e(\"登录结果：\" + result + \" , \" + loginResult.getError() + loginResult.getAuth());\n\n                    if (!loginResult.success) {\n                        cb.onFailure(loginResult.getError());\n                    }else {\n                        cb.onSuccess(loginResult.getAuth());\n                    }\n                }else {\n                    cb.onFailure(response.message());\n                }\n            }\n\n            @Override\n            public void onFailure(retrofit2.Call<String> call, Throwable t) {\n                cb.onFailure(t.getMessage());\n            }\n        });\n    }\n\n\n    @Override\n    public void setAuthorization(String authorization) {\n        super.setAuthorization(authorization);\n    }\n\n    public String getOAuthUrl() {\n//        String baseUrl = HostConfig.i().getRedirectUrl(OFFICIAL_BASE_URL);\n//        if(StringUtils.isEmpty(baseUrl)){\n//            baseUrl = OFFICIAL_BASE_URL;\n//        }\n        return OFFICIAL_BASE_URL + \"oauth2/auth?response_type=code&client_id=\" + APP_ID + \"&redirect_uri=\" + REDIRECT_URI + \"&state=loread&lang=\" + Locale.getDefault();\n    }\n\n    public void getAccessToken(String authorizationCode,CallbackX cb) {\n        FormBody.Builder builder = new FormBody.Builder();\n        builder.add(\"grant_type\", \"authorization_code\");\n        builder.add(\"code\", authorizationCode);\n        builder.add(\"redirect_uri\", REDIRECT_URI);\n        builder.add(\"client_id\", APP_ID);\n        builder.add(\"client_secret\", APP_KEY);\n\n        service.getAccessToken(\"authorization_code\", REDIRECT_URI,APP_ID,APP_KEY,authorizationCode).enqueue(new Callback<Token>() {\n            @Override\n            public void onResponse(@NotNull Call<Token> call, @NotNull Response<Token> response) {\n                if(response.isSuccessful()){\n                    cb.onSuccess(response.body());\n                }else {\n                    cb.onFailure(\"失败：\" + response.message());\n                }\n            }\n\n            @Override\n            public void onFailure(@NotNull Call<Token> call, @NotNull Throwable t) {\n                cb.onFailure(\"失败：\" + t.getMessage());\n            }\n        });\n    }\n\n    public void refreshingAccessToken(String refreshToken, CallbackX cb) {\n        service.refreshingAccessToken(\"refresh_token\",refreshToken,APP_ID,APP_KEY).enqueue(new Callback<Token>() {\n            @Override\n            public void onResponse(@NotNull Call<Token> call, @NotNull Response<Token> response) {\n                if(response.isSuccessful() && response.body()!=null){\n                    if (TextUtils.isEmpty(response.body().getRefresh_token())) {\n                        response.body().setRefresh_token(refreshToken);\n                    }\n                    User user = App.i().getUser();\n                    if (user != null) {\n                        user.setToken(response.body());\n                        CoreDB.i().userDao().insert(user);\n                    }\n                    // 更新缓存中的授权\n                    ((FeedlyApi) App.i().getApi()).setAuthorization(App.i().getUser().getAuth());\n\n                    cb.onSuccess(response.body());\n                }else {\n                    cb.onFailure(\"失败：\" + response.message());\n                }\n            }\n\n            @Override\n            public void onFailure(Call<Token> call, Throwable t) {\n                cb.onFailure(\"失败：\" + t.getMessage());\n            }\n        });\n    }\n\n    public String refreshingAccessToken(String refreshToken) throws IOException {\n        Token token = service.refreshingAccessToken(\"refresh_token\",refreshToken,APP_ID,APP_KEY).execute().body();\n        if (TextUtils.isEmpty(token.getRefresh_token())) {\n            token.setRefresh_token(refreshToken);\n        }\n        User user = App.i().getUser();\n        if (user != null) {\n            user.setToken(token);\n            CoreDB.i().userDao().insert(user);\n        }\n        // 更新缓存中的授权\n        ((FeedlyApi) App.i().getApi()).setAuthorization(App.i().getUser().getAuth());\n        return token.getAuth();\n    }\n\n    /**\n     * 一般用在首次登录的时候，去获取用户基本资料\n     *\n     * @return\n     * @throws IOException\n     */\n    public void fetchUserInfo( CallbackX cb){\n        service.getUserInfo(getAuthorization()).enqueue(new Callback<UserInfo>() {\n            @Override\n            public void onResponse(@NonNull Call<UserInfo> call,@NonNull Response<UserInfo> response) {\n                KLog.e(\"获取响应\" + getAuthorization() + response.message() );\n                KLog.e(\"获取响应\" + response );\n                if( response.isSuccessful()){\n                    KLog.e(\"获取响应成功\" );\n                    cb.onSuccess(response.body().getUser());\n                }else {\n                    KLog.e(\"获取响应失败\");\n                    cb.onFailure(\"获取失败：\" + response.message());\n                }\n            }\n            @Override\n            public void onFailure(@NonNull Call<UserInfo> call,@NonNull Throwable t) {\n                KLog.e(\"响应失败\");\n                cb.onFailure(\"获取失败：\" + t.getMessage() + t.toString());\n                t.printStackTrace();\n            }\n        });\n    }\n\n    @Override\n    public void sync() {\n        try {\n            long startSyncTimeMillis = System.currentTimeMillis();\n            String uid = App.i().getUser().getId();\n\n            KLog.e(\"3 - 同步订阅源信息\");\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post( App.i().getString(R.string.sync_feed_info) );\n\n            // 获取分类\n            List<Category> categories = service.getCategoryItems(getAuthorization()).execute().body().getCategories();\n            // 去除分类中的的一些无用分类\n            if (categories != null && categories.size() > 0 && categories.get(0).getId().endsWith(\"/state/com.google/starred\")) {\n                categories.remove(0); // /state/com.google/starred\n            }\n            if (categories != null && categories.size() > 0 && categories.get(0).getId().endsWith(\"/state/com.google/broadcast\")) {\n                categories.remove(0); // /state/com.google/broadcast\n            }\n            if (categories != null && categories.size() > 0 && categories.get(0).getId().endsWith(\"/state/com.google/blogger-following\")) {\n                categories.remove(0); // /state/com.google/blogger-following\n            }\n            String[] array;\n            String tagTitle;\n            for (Category category : categories) {\n                array = category.getId().split(\"/\");\n                tagTitle = array[array.length - 1];\n                category.setTitle(tagTitle);\n            }\n\n            // 获取feed\n            List<Subscription> subscriptions = service.getFeeds(getAuthorization()).execute().body().getSubscriptions();\n            List<Feed> feeds = new ArrayList<>(subscriptions.size());\n            Feed feed;\n            List<FeedCategory> feedCategories = new ArrayList<>();\n            FeedCategory feedCategory;\n            for (Subscription subscription : subscriptions) {\n                feed = subscription.convert2Feed();\n                feed.setUid(uid);\n                feeds.add(feed);\n                for (SubCategories subCategories : subscription.getCategories()) {\n                    feedCategory = new FeedCategory(uid, subscription.getId(), subCategories.getId());\n                    feedCategories.add(feedCategory);\n                }\n            }\n\n            // 如果在获取到数据的时候就保存，那么到这里同步断了的话，可能系统内的文章就找不到响应的分组，所有放到这里保存。\n            // （比如在云端将文章移到的新的分组）\n            coverSaveFeeds(feeds);\n            coverSaveCategories(categories);\n            coverFeedCategory(feedCategories);\n\n            KLog.e(\"2 - 同步文章信息\");\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post( App.i().getString(R.string.sync_article_refs) );\n            // 获取未读资源\n            HashSet<String> unreadRefsList = fetchUnreadRefs();\n            // 获取加星资源\n            HashSet<String> staredRefsList = fetchStaredRefs();\n\n            KLog.e(\"1 - 同步文章内容\");\n            ArrayList<HashSet<String>> refsList = splitRefs(unreadRefsList, staredRefsList);\n            int allSize = refsList.get(0).size() + refsList.get(1).size() + refsList.get(2).size();\n\n            // 抓取【未读、未加星】文章\n            fetchArticle(allSize, 0, new ArrayList<>(refsList.get(0)), new ArticleChanger() {\n                @Override\n                public Article change(Article article) {\n                    article.setCrawlDate(System.currentTimeMillis());\n                    article.setReadStatus(App.STATUS_UNREAD);\n                    article.setStarStatus(App.STATUS_UNSTAR);\n                    article.setUid(uid);\n                    return article;\n                }\n            });\n            // 抓取【已读、已加星】文章\n            fetchArticle(allSize, refsList.get(0).size(), new ArrayList<>(refsList.get(1)), new ArticleChanger() {\n                @Override\n                public Article change(Article article) {\n                    article.setCrawlDate(System.currentTimeMillis());\n                    article.setReadStatus(App.STATUS_READED);\n                    article.setStarStatus(App.STATUS_STARED);\n                    article.setUid(uid);\n                    return article;\n                }\n            });\n\n            // 抓取【未读、已加星】文章\n            fetchArticle(allSize, refsList.get(0).size() + refsList.get(1).size(), new ArrayList<>(refsList.get(2)), new ArticleChanger() {\n                @Override\n                public Article change(Article article) {\n                    article.setCrawlDate(System.currentTimeMillis());\n                    article.setReadStatus(App.STATUS_UNSTAR);\n                    article.setStarStatus(App.STATUS_STARED);\n                    article.setUid(uid);\n                    return article;\n                }\n            });\n\n\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post(getString(R.string.clear_article));\n            deleteExpiredArticles();\n\n            // 获取文章全文\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post( App.i().getString(R.string.fetch_article_full_content) );\n            fetchReadability(uid, startSyncTimeMillis);\n\n            // 为所有新增的加星文章自动生成tag\n            handleNotTagStarArticles(uid, startSyncTimeMillis);\n            // 执行文章自动处理脚本\n            ArticleActionConfig.i().exeRules(uid,startSyncTimeMillis);\n            // 清理无文章的tag\n            //clearNotArticleTags(uid);\n\n            // 提示更新完成\n            LiveEventBus.get(SyncWorker.NEW_ARTICLE_NUMBER).post(allSize);\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post( null );\n        } catch (IOException e) {\n            KLog.e(\"错误\");\n            e.printStackTrace();\n            if (e.getMessage().equals(\"401\")) {\n                ToastUtils.show(\"网络异常，请重新登录\");\n            }\n        }\n\n        handleDuplicateArticle();\n        handleCrawlDate();\n        updateCollectionCount();\n        LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post( null );\n    }\n\n\n    private void fetchArticle(int allSize, int syncedSize, List<String> subIds, ArticleChanger articleChanger) throws IOException{\n        int needFetchCount = subIds.size();\n        int hadFetchCount = 0;\n\n        while (needFetchCount > 0) {\n            int fetchUnit = Math.min(needFetchCount, fetchContentCntForEach);\n            List<Item> items = service.getItemContents(getAuthorization(), genRequestBody(subIds.subList(hadFetchCount, hadFetchCount = hadFetchCount + fetchUnit))).execute().body().getItems();\n            List<Article> tempArticleList = new ArrayList<>(fetchUnit);\n            for (Item item : items) {\n                tempArticleList.add(item.convert(articleChanger));\n            }\n            CoreDB.i().articleDao().insert(tempArticleList);\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post( App.i().getString(R.string.sync_article_content, syncedSize = syncedSize + fetchUnit, allSize) );\n            needFetchCount = subIds.size() - hadFetchCount;\n        }\n    }\n\n    private HashSet<String> fetchUnreadRefs() throws IOException {\n        List<ItemRefs> itemRefs = new ArrayList<>();\n        // String info;\n        ItemIds tempItemIds = new ItemIds();\n        int i = 0;\n        do {\n            tempItemIds = service.getStreamItemsIds(getAuthorization(),\"user/-/state/com.google/reading-list\", \"user/-/state/com.google/read\", 1000, false, tempItemIds.getContinuation()).execute().body();\n            i++;\n            itemRefs.addAll(tempItemIds.getItemRefs());\n        } while (tempItemIds.getContinuation() != null && i < 5);\n        Collections.reverse(itemRefs); // 倒序排列\n\n        List<Article> localUnreadArticles = CoreDB.i().articleDao().getUnreadNoOrder(App.i().getUser().getId());\n        Map<String, Article> localUnreadArticlesMap = new ArrayMap<>(localUnreadArticles.size());\n        List<Article> changedArticles = new ArrayList<>();\n        // 筛选下来，最终要去云端获取内容的未读Refs的集合\n        HashSet<String> tempUnreadIds = new HashSet<>(itemRefs.size());\n        // 数据量大的一方\n        String articleId;\n        for (Article article : localUnreadArticles) {\n            articleId = article.getId();\n            localUnreadArticlesMap.put(articleId, article);\n        }\n        // 数据量小的一方\n        Article article;\n        for (ItemRefs item : itemRefs) {\n            articleId = item.getLongId();\n            article = localUnreadArticlesMap.get(articleId);\n            if (article != null) {\n                localUnreadArticlesMap.remove(articleId);\n            } else {\n                article = CoreDB.i().articleDao().getById(App.i().getUser().getId(), articleId);\n                if (article != null && article.getReadStatus() == App.STATUS_READED) {\n                    article.setReadStatus(App.STATUS_UNREAD);\n                    changedArticles.add(article);\n                } else {\n                    // 本地无，而云端有，加入要请求的未读资源\n                    tempUnreadIds.add(articleId);\n                }\n            }\n        }\n        for (Map.Entry<String, Article> entry : localUnreadArticlesMap.entrySet()) {\n            if (entry.getKey() != null) {\n                article = localUnreadArticlesMap.get(entry.getKey());\n                // 本地未读设为已读\n                article.setReadStatus(App.STATUS_READED);\n                changedArticles.add(article);\n            }\n        }\n\n        CoreDB.i().articleDao().update(changedArticles);\n        return tempUnreadIds;\n    }\n\n    private HashSet<String> fetchStaredRefs() throws HttpException, IOException {\n        List<ItemRefs> itemRefs = new ArrayList<>();\n        String info;\n        ItemIds tempItemIds = new ItemIds();\n        int i = 0;\n        do {\n            tempItemIds = service.getStreamItemsIds(getAuthorization(),\"user/-/state/com.google/starred\", null, 1000, false, tempItemIds.getContinuation()).execute().body();\n            i++;\n            itemRefs.addAll(tempItemIds.getItemRefs());\n        } while (tempItemIds.getContinuation() != null && i < 5);\n        Collections.reverse(itemRefs); // 倒序排列\n\n        List<Article> localStarredArticles = CoreDB.i().articleDao().getStaredNoOrder(App.i().getUser().getId());\n        Map<String, Article> localStarredArticlesMap = new ArrayMap<>(localStarredArticles.size());\n        List<Article> changedArticles = new ArrayList<>();\n        HashSet<String> tempStarredIds = new HashSet<>(itemRefs.size());\n\n        String articleId;\n        // 第1步，遍历数据量大的一方A，将其比对项目放入Map中\n        for (Article article : localStarredArticles) {\n            articleId = article.getId();\n            localStarredArticlesMap.put(articleId, article);\n        }\n\n        // 第2步，遍历数据量小的一方B。到Map中找，是否含有b中的比对项。有则XX，无则YY\n        Article article;\n        for (ItemRefs item : itemRefs) {\n            articleId = item.getLongId();\n            article = localStarredArticlesMap.get(articleId);\n            if (article != null) {\n                localStarredArticlesMap.remove(articleId);\n            } else {\n                article = CoreDB.i().articleDao().getById(App.i().getUser().getId(), articleId);\n                if (article != null) {\n                    article.setStarStatus(App.STATUS_STARED);\n                    changedArticles.add(article);\n                } else {\n                    // 本地无，而云远端有，加入要请求的未读资源\n                    tempStarredIds.add(articleId);\n                }\n            }\n        }\n\n        for (Map.Entry<String, Article> entry : localStarredArticlesMap.entrySet()) {\n            if (entry.getKey() != null) {\n                article = localStarredArticlesMap.get(entry.getKey());\n                article.setStarStatus(App.STATUS_UNSTAR);\n                changedArticles.add(article);// 取消加星\n            }\n        }\n\n        CoreDB.i().articleDao().update(changedArticles);\n        return tempStarredIds;\n    }\n\n    private RequestBody genRequestBody(List<String> ids) {\n        FormBody.Builder builder = new FormBody.Builder();\n        for (String id : ids) {\n            builder.add(\"i\", id);\n        }\n        return builder.build();\n    }\n\n    public void renameTag(String sourceTagId, String targetName, CallbackX cb) {\n        FormBody.Builder builder = new FormBody.Builder();\n        builder.add(\"s\", sourceTagId);\n        builder.add(\"dest\", targetName);\n        //WithHttp.i().asyncPost(HOST + \"/reader/api/0/rename-tag\", builder, authHeaders, cb);\n        service.renameTag(getAuthorization(), builder.build()).enqueue(new Callback<String>() {\n            @Override\n            public void onResponse(Call<String> call, Response<String> response) {\n                if( response.isSuccessful()){\n                    String msg = response.body();\n                    if(\"OK\".equals(msg)){\n                        cb.onSuccess(\"修改成功\");\n                    }else if( msg.contains(\"Tag not found!\")){\n                        cb.onFailure(\"修改失败：要修改分类不存在\");\n                    }\n                }else {\n                    cb.onFailure(\"修改失败：未知原因\");\n                }\n            }\n\n            @Override\n            public void onFailure(Call<String> call, Throwable t) {\n                cb.onFailure(\"修改失败：\" + t.getMessage());\n            }\n        });\n    }\n\n    public void unsubscribeFeed(String feedId,CallbackX cb) {\n        FormBody.Builder builder = new FormBody.Builder();\n        builder.add(\"ac\", \"unsubscribe\");\n        builder.add(\"s\", feedId);\n        service.editFeed(getAuthorization(), builder.build()).enqueue(new Callback<String>() {\n            @Override\n            public void onResponse(Call<String> call, Response<String> response) {\n                if (response.isSuccessful() ){\n                    String msg = response.body();\n                    if( !TextUtils.isEmpty(msg) && msg.contains(\"OK\")){\n                        cb.onSuccess(null);\n                    }else {\n                        cb.onFailure(msg);\n                    }\n                }else {\n                    cb.onFailure(\"修改失败：原因未知\");\n                }\n            }\n\n            @Override\n            public void onFailure(Call<String> call, Throwable t) {\n                cb.onFailure(t.getMessage());\n            }\n        });\n    }\n\n    public void addFeed(@NonNull EditFeed editFeed, CallbackX cb) {\n        FormBody.Builder builder = new FormBody.Builder();\n        builder.add(\"ac\", \"subscribe\");\n        builder.add(\"s\", editFeed.getId());\n        List<CategoryItem> categoryItemList = editFeed.getCategoryItems();\n        for (CategoryItem categoryItem : categoryItemList) {\n            builder.add(\"a\", categoryItem.getId());\n        }\n        service.editFeed(getAuthorization(), builder.build()).enqueue(new Callback<String>() {\n            @Override\n            public void onResponse(Call<String> call, Response<String> response) {\n                if (response.isSuccessful()) {\n                    KLog.e(\"添加成功\" + response.body().toString());\n                    cb.onSuccess(\"添加成功\");\n                } else {\n                    cb.onFailure(\"响应失败\");\n                }\n            }\n\n            @Override\n            public void onFailure(Call<String> call, Throwable t) {\n                cb.onFailure(t.getMessage());\n            }\n        });\n    }\n\n    public void renameFeed(String feedId, String renamedTitle, CallbackX cb) {\n        FormBody.Builder builder = new FormBody.Builder();\n//        builder.add(\"ac\", \"edit\"); // 可省略\n        builder.add(\"s\", feedId);\n        builder.add(\"t\", renamedTitle);\n        service.editFeed(getAuthorization(), builder.build()).enqueue(new Callback<String>() {\n            @Override\n            public void onResponse(@NonNull Call<String> call, @NonNull Response<String> response) {\n                if (response.isSuccessful() ){\n                    String msg = response.body();\n                    if( !TextUtils.isEmpty(msg) && msg.contains(\"OK\")){\n                        cb.onSuccess(null);\n                    }else {\n                        cb.onFailure(msg);\n                    }\n                }else {\n                    cb.onFailure(\"修改失败：原因未知\");\n                }\n            }\n            @Override\n            public void onFailure(@NonNull Call<String> call, @NonNull Throwable t) {\n                cb.onFailure(t.getMessage());\n            }\n        });\n    }\n\n    @Override\n    public void editFeedCategories(List<CategoryItem> lastCategoryItems, EditFeed editFeed, CallbackX cb) {\n        FormBody.Builder builder = new FormBody.Builder();\n        builder.add(\"ac\", \"edit\");\n        builder.add(\"s\", editFeed.getId());\n\n        ArrayList<CategoryItem> selectedCategoryItems = editFeed.getCategoryItems();\n        ArrayMap<String, CategoryItem> lastCategoryItemsMap = new ArrayMap<>(lastCategoryItems.size());\n        for (CategoryItem categoryItem : lastCategoryItems) {\n            lastCategoryItemsMap.put(categoryItem.getId(), categoryItem);\n        }\n        for (CategoryItem categoryItem : selectedCategoryItems) {\n            if (lastCategoryItemsMap.get(categoryItem.getId()) == null) {\n                builder.add(\"a\", categoryItem.getId());\n                lastCategoryItemsMap.remove(categoryItem);\n            }\n        }\n        for (Map.Entry<String, CategoryItem> entry : lastCategoryItemsMap.entrySet()) {\n            builder.add(\"r\", entry.getKey());\n        }\n        //WithHttp.i().asyncPost(HOST + \"/reader/api/0/subscription/edit\", builder, authHeaders, cb);\n        service.editFeed(getAuthorization(), builder.build()).enqueue(new Callback<String>() {\n            @Override\n            public void onResponse(Call<String> call, Response<String> response) {\n                if (response.isSuccessful() ){\n                    String msg = response.body();\n                    if( !TextUtils.isEmpty(msg) && msg.equalsIgnoreCase(\"ok\")){\n                        cb.onSuccess(null);\n                    }else {\n                        cb.onFailure(msg);\n                    }\n                }else {\n                    cb.onFailure(\"修改失败：原因未知\");\n                }\n            }\n\n            @Override\n            public void onFailure(Call<String> call, Throwable t) {\n                cb.onFailure(t.getMessage());\n            }\n        });\n    }\n\n\n    public void markArticleListReaded(List<String> articleIDs,CallbackX cb) {\n        FormBody.Builder builder = new FormBody.Builder();\n        builder.add(\"a\", \"user/-/state/com.google/read\");\n        for (String articleID : articleIDs) {\n            builder.add(\"i\", articleID);\n        }\n        service.markArticle(getAuthorization(), builder.build()).enqueue(new Callback<String>() {\n            @Override\n            public void onResponse(Call<String> call, Response<String> response) {\n                if (response.isSuccessful() ){\n                    String msg = response.body();\n                    if( !TextUtils.isEmpty(msg) && msg.equalsIgnoreCase(\"ok\")){\n                        cb.onSuccess(null);\n                    }else {\n                        cb.onFailure(msg);\n                    }\n                }else {\n                    cb.onFailure(\"修改失败：原因未知\");\n                }\n            }\n\n            @Override\n            public void onFailure(Call<String> call, Throwable t) {\n                cb.onFailure(t.getMessage());\n            }\n        });\n    }\n\n    @Override\n    public void markArticleReaded(String articleID, CallbackX cb) {\n        FormBody.Builder builder = new FormBody.Builder();\n        builder.add(\"a\", \"user/-/state/com.google/read\");\n        builder.add(\"i\", articleID);\n        service.markArticle(getAuthorization(), builder.build()).enqueue(new Callback<String>() {\n            @Override\n            public void onResponse(Call<String> call, Response<String> response) {\n                if (response.isSuccessful() ){\n                    String msg = response.body();\n                    if( !TextUtils.isEmpty(msg) && msg.equalsIgnoreCase(\"ok\")){\n                        cb.onSuccess(null);\n                    }else {\n                        cb.onFailure(msg);\n                    }\n                }else {\n                    cb.onFailure(\"修改失败：原因未知\");\n                }\n            }\n\n            @Override\n            public void onFailure(Call<String> call, Throwable t) {\n                cb.onFailure(t.getMessage());\n            }\n        });\n    }\n\n    public void markArticleUnread(String articleID,CallbackX cb) {\n        FormBody.Builder builder = new FormBody.Builder();\n        builder.add(\"r\", \"user/-/state/com.google/read\");\n        builder.add(\"i\", articleID);\n        service.markArticle(getAuthorization(), builder.build()).enqueue(new Callback<String>() {\n            @Override\n            public void onResponse(Call<String> call, Response<String> response) {\n                if (response.isSuccessful() ){\n                    String msg = response.body();\n                    if( !TextUtils.isEmpty(msg) && msg.equalsIgnoreCase(\"ok\")){\n                        cb.onSuccess(null);\n                    }else {\n                        cb.onFailure(msg);\n                    }\n                }else {\n                    cb.onFailure(\"修改失败：原因未知\");\n                }\n            }\n\n            @Override\n            public void onFailure(Call<String> call, Throwable t) {\n                cb.onFailure(t.getMessage());\n            }\n        });\n    }\n\n\n    public void markArticleStared(String articleID,CallbackX cb) {\n        FormBody.Builder builder = new FormBody.Builder();\n        builder.add(\"a\", \"user/-/state/com.google/starred\");\n        builder.add(\"i\", articleID);\n        service.markArticle(getAuthorization(), builder.build()).enqueue(new Callback<String>() {\n            @Override\n            public void onResponse(Call<String> call, Response<String> response) {\n                if (response.isSuccessful() ){\n                    String msg = response.body();\n                    if( !TextUtils.isEmpty(msg) && msg.equalsIgnoreCase(\"ok\")){\n                        cb.onSuccess(null);\n                    }else {\n                        cb.onFailure(msg);\n                    }\n                }else {\n                    cb.onFailure(\"修改失败：原因未知\");\n                }\n            }\n\n            @Override\n            public void onFailure(Call<String> call, Throwable t) {\n                cb.onFailure(t.getMessage());\n            }\n        });\n    }\n\n\n    public void markArticleUnstar(String articleID,CallbackX cb) {\n        FormBody.Builder builder = new FormBody.Builder();\n        builder.add(\"r\", \"user/-/state/com.google/starred\");\n        builder.add(\"i\", articleID);\n        service.markArticle(getAuthorization(), builder.build()).enqueue(new Callback<String>() {\n            @Override\n            public void onResponse(Call<String> call, Response<String> response) {\n                if (response.isSuccessful() ){\n                    String msg = response.body();\n                    if( !TextUtils.isEmpty(msg) && msg.equalsIgnoreCase(\"ok\")){\n                        cb.onSuccess(null);\n                    }else {\n                        cb.onFailure(msg);\n                    }\n                }else {\n                    cb.onFailure(\"修改失败：原因未知\");\n                }\n            }\n\n            @Override\n            public void onFailure(Call<String> call, Throwable t) {\n                cb.onFailure(t.getMessage());\n            }\n        });\n    }\n\n    /**\n     * 将 未读资源 和 加星资源，去重分为3组\n     *\n     * @param tempUnreadIds\n     * @param tempStarredIds\n     * @return\n     */\n    public ArrayList<HashSet<String>> splitRefs(HashSet<String> tempUnreadIds, HashSet<String> tempStarredIds) {\n//        KLog.e(\"【reRefs1】云端未读\" + tempUnreadIds.size() + \"，云端加星\" + tempStarredIds.size());\n        int total = Math.min(tempUnreadIds.size(), tempStarredIds.size());\n\n        HashSet<String> reUnreadUnstarRefs;\n        HashSet<String> reReadStarredRefs = new HashSet<>(tempStarredIds.size());\n        HashSet<String> reUnreadStarredRefs = new HashSet<>(total);\n\n        for (String id : tempStarredIds) {\n            if (tempUnreadIds.contains(id)) {\n                tempUnreadIds.remove(id);\n                reUnreadStarredRefs.add(id);\n            } else {\n                reReadStarredRefs.add(id);\n            }\n        }\n        reUnreadUnstarRefs = tempUnreadIds;\n\n        ArrayList<HashSet<String>> refsList = new ArrayList<>();\n        refsList.add(reUnreadUnstarRefs);\n        refsList.add(reReadStarredRefs);\n        refsList.add(reUnreadStarredRefs);\n//        KLog.e(\"【reRefs2】\" + reUnreadUnstarRefs.size() + \"--\" + reReadStarredRefs.size() + \"--\" + reUnreadStarredRefs.size());\n        return refsList;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/api/InoReaderService.java",
    "content": "package me.wizos.loread.network.api;\n\nimport androidx.annotation.NonNull;\n\nimport me.wizos.loread.bean.Token;\nimport me.wizos.loread.bean.inoreader.GsTags;\nimport me.wizos.loread.bean.inoreader.ItemIds;\nimport me.wizos.loread.bean.inoreader.StreamContents;\nimport me.wizos.loread.bean.inoreader.Subscriptions;\nimport me.wizos.loread.bean.inoreader.UserInfo;\nimport okhttp3.RequestBody;\nimport retrofit2.Call;\nimport retrofit2.http.Body;\nimport retrofit2.http.Field;\nimport retrofit2.http.FormUrlEncoded;\nimport retrofit2.http.GET;\nimport retrofit2.http.Header;\nimport retrofit2.http.POST;\nimport retrofit2.http.Query;\n\n/**\n * Created by Wizos on 2019/5/12.\n */\n\npublic interface InoReaderService {\n    @FormUrlEncoded\n    @POST(\"oauth2/token\")\n    Call<Token> getAccessToken(\n            @Field(\"grant_type\") String grantType,\n            @Field(\"redirect_uri\") String redirectUri,\n            @Field(\"client_id\") String clientId,\n            @Field(\"client_secret\") String clientSecret,\n            @Field(\"code\") String code\n    );\n\n    @FormUrlEncoded\n    @POST(\"oauth2/token\")\n    Call<Token> refreshingAccessToken(\n            @Field(\"grant_type\") String grantType,\n            @Field(\"refresh_token\") String refreshToken,\n            @Field(\"client_id\") String clientId,\n            @Field(\"client_secret\") String clientSecret\n    );\n\n\n    @FormUrlEncoded\n    @POST(\"accounts/ClientLogin\")\n    Call<String> login(\n            @Field(\"Email\") String email,\n            @Field(\"Passwd\") String password\n    );\n\n    @GET(\"/reader/api/0/user-info\")\n    Call<UserInfo> getUserInfo(\n            @Header(\"authorization\") String authorization\n    );\n\n\n    @GET(\"reader/api/0/tag/list\")\n    Call<GsTags> getCategoryItems(\n            @Header(\"authorization\") String authorization\n    );\n\n    @GET(\"reader/api/0/subscription/list\")\n    Call<Subscriptions> getFeeds(\n            @Header(\"authorization\") String authorization\n    );\n\n    @GET(\"reader/api/0/stream/items/ids\")\n    Call<ItemIds> getStreamItemsIds(\n            @Header(\"authorization\") String authorization,\n            @Query(\"s\") String s,\n            @Query(\"xt\") String xt,\n            @Query(\"n\") int count,\n            @Query(\"includeAllDirectStreamIds\") boolean includeAllDirectStreamIds,\n            @Query(\"continuation\") String continuation\n    );\n\n    @GET(\"reader/api/0/stream/items/ids\")\n    Call<ItemIds> getUnreadRefs(\n            @Header(\"authorization\") String authorization,\n            @Query(\"s\") String s,\n            @Query(\"xt\") String xt,\n            @Query(\"n\") int count,\n            @Query(\"includeAllDirectStreamIds\") boolean includeAllDirectStreamIds,\n            @Query(\"continuation\") String continuation\n    );\n\n    @GET(\"reader/api/0/stream/items/ids\")\n    Call<ItemIds> syncStarredRefs(\n            @Header(\"authorization\") String authorization,\n            @Query(\"s\") String s,\n            @Query(\"xt\") String xt,\n            @Query(\"n\") int count,\n            @Query(\"includeAllDirectStreamIds\") boolean includeAllDirectStreamIds,\n            @Query(\"continuation\") String continuation\n    );\n\n    @POST(\"reader/api/0/stream/items/contents\")\n    Call<StreamContents> getItemContents(\n            @Header(\"authorization\") String authorization,\n            @NonNull @Body RequestBody requestBody\n    );\n\n\n    @POST(\"reader/api/0/subscription/quickadd\")\n    Call<String> addFeed(\n            @NonNull @Body RequestBody requestBody\n    );\n\n    @POST(\"reader/api/0/subscription/edit\")\n    Call<String> editFeed(\n            @Header(\"authorization\") String authorization,\n            @NonNull @Body RequestBody requestBody\n    );\n\n\n    @POST(\"reader/api/0/edit-tag\")\n    Call<String> markArticle(\n            @Header(\"authorization\") String authorization,\n            @Body RequestBody requestBody\n    );\n\n    // 失败返回：Error=Tag not found!\n    // 成功返回：OK\n    @POST(\"reader/api/0/rename-tag\")\n    Call<String> renameTag(\n            @Header(\"authorization\") String authorization,\n            @Body RequestBody requestBody\n    );\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/api/LoginInterface.java",
    "content": "package me.wizos.loread.network.api;\nimport me.wizos.loread.network.callback.CallbackX;\n\npublic interface LoginInterface {\n    void login(String accountId, String accountPd, CallbackX cb);\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/api/LoreadApi.java",
    "content": "package me.wizos.loread.network.api;\n\nimport android.text.TextUtils;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.collection.ArrayMap;\n\nimport com.google.gson.GsonBuilder;\nimport com.hjq.toast.ToastUtils;\nimport com.jeremyliao.liveeventbus.LiveEventBus;\nimport com.lzy.okgo.callback.StringCallback;\nimport com.lzy.okgo.exception.HttpException;\nimport com.socks.library.KLog;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.io.IOException;\nimport java.net.ConnectException;\nimport java.net.SocketTimeoutException;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.activity.login.LoginResult;\nimport me.wizos.loread.bean.feedly.input.EditFeed;\nimport me.wizos.loread.bean.ttrss.request.GetArticles;\nimport me.wizos.loread.bean.ttrss.request.GetCategories;\nimport me.wizos.loread.bean.ttrss.request.GetFeeds;\nimport me.wizos.loread.bean.ttrss.request.GetHeadlines;\nimport me.wizos.loread.bean.ttrss.request.GetSavedItemIds;\nimport me.wizos.loread.bean.ttrss.request.GetUnreadItemIds;\nimport me.wizos.loread.bean.ttrss.request.LoginParam;\nimport me.wizos.loread.bean.ttrss.request.SubscribeToFeed;\nimport me.wizos.loread.bean.ttrss.request.UnsubscribeFeed;\nimport me.wizos.loread.bean.ttrss.request.UpdateArticle;\nimport me.wizos.loread.bean.ttrss.result.ArticleItem;\nimport me.wizos.loread.bean.ttrss.result.CategoryItem;\nimport me.wizos.loread.bean.ttrss.result.FeedItem;\nimport me.wizos.loread.bean.ttrss.result.SubscribeToFeedResult;\nimport me.wizos.loread.bean.ttrss.result.TTRSSLoginResult;\nimport me.wizos.loread.bean.ttrss.result.TinyResponse;\nimport me.wizos.loread.bean.ttrss.result.UpdateArticleResult;\nimport me.wizos.loread.config.ArticleActionConfig;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.db.Category;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.Feed;\nimport me.wizos.loread.db.FeedCategory;\nimport me.wizos.loread.network.HttpClientManager;\nimport me.wizos.loread.network.SyncWorker;\nimport me.wizos.loread.network.callback.CallbackX;\nimport me.wizos.loread.utils.StringUtils;\nimport retrofit2.Response;\nimport retrofit2.Retrofit;\nimport retrofit2.converter.gson.GsonConverterFactory;\n\nimport static me.wizos.loread.utils.StringUtils.getString;\n\n/**\n * Created by Wizos on 2019/2/8.\n */\n\npublic class LoreadApi extends AuthApi<Feed, me.wizos.loread.bean.feedly.CategoryItem> implements LoginInterface{\n    private LoreadService service;\n    private static String HOST = \"\";\n\n    public LoreadApi() {\n        if (TextUtils.isEmpty(HOST)) {\n            LoreadApi.HOST = App.i().getUser().getHost();\n            KLog.e(\"HOST 地址：\" + HOST );\n        }\n        Retrofit retrofit = new Retrofit.Builder()\n                .baseUrl(LoreadApi.HOST) // 设置网络请求的Url地址, 必须以/结尾\n                .addConverterFactory(GsonConverterFactory.create(new GsonBuilder().setLenient().create()))  // 设置数据解析器\n                .client(HttpClientManager.i().loreadHttpClient())\n                .build();\n        service = retrofit.create(LoreadService.class);\n    }\n\n    public static void setHost(String host) {\n        LoreadApi.HOST = host;\n        KLog.e(\"HOST 地址：\" + host );\n    }\n\n    public LoginResult login(String accountId, String accountPd) throws IOException {\n        LoginParam loginParam = new LoginParam();\n        loginParam.setUser(accountId);\n        loginParam.setPassword(accountPd);\n        TinyResponse<TTRSSLoginResult> loginResultTTRSSResponse = service.login(loginParam).execute().body();\n        LoginResult loginResult = new LoginResult();\n        if (loginResultTTRSSResponse.isSuccessful()) {\n            return loginResult.setSuccess(true).setData(loginResultTTRSSResponse.getContent().getSession_id());\n        } else {\n            return loginResult.setSuccess(false).setData(loginResultTTRSSResponse.getContent().getSession_id());\n        }\n    }\n\n    public void login(String accountId, String accountPd,CallbackX cb){\n        LoginParam loginParam = new LoginParam();\n        loginParam.setUser(accountId);\n        loginParam.setPassword(accountPd);\n        service.login(loginParam).enqueue(new retrofit2.Callback<TinyResponse<TTRSSLoginResult>>() {\n            @Override\n            public void onResponse(retrofit2.Call<TinyResponse<TTRSSLoginResult>> call, Response<TinyResponse<TTRSSLoginResult>> response) {\n                if(response.isSuccessful()){\n                    TinyResponse<TTRSSLoginResult> loginResultTTRSSResponse = response.body();\n                    if( loginResultTTRSSResponse.isSuccessful()){\n                        cb.onSuccess(loginResultTTRSSResponse.getContent().getSession_id());\n                        return;\n                    }\n                    cb.onFailure(\"登录失败：原因未知\" + loginResultTTRSSResponse.toString());\n                }else {\n                    cb.onFailure(\"登录失败：原因未知\" + response.message());\n                }\n            }\n\n            @Override\n            public void onFailure(retrofit2.Call<TinyResponse<TTRSSLoginResult>> call, Throwable t) {\n                cb.onFailure(\"登录失败：\" + t.getMessage());\n            }\n        });\n    }\n\n    public void fetchUserInfo(CallbackX cb){\n        cb.onFailure(\"暂时不支持\");\n    }\n\n    @Override\n    public void sync() {\n        try {\n            long startSyncTimeMillis = System.currentTimeMillis();\n            String uid = App.i().getUser().getId();\n\n            KLog.e(\"3 - 同步订阅源信息：获取分类\");\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post(getString(R.string.sync_feed_info));\n\n            // 获取分类\n            TinyResponse<List<CategoryItem>> categoryItemsTTRSSResponse = service.getCategories(getAuthorization(),new GetCategories(getAuthorization())).execute().body();\n            KLog.e(\"获取回应：\" + categoryItemsTTRSSResponse);\n            if (!categoryItemsTTRSSResponse.isSuccessful()) {\n                throw new HttpException(\"获取失败\");\n            }\n\n            Iterator<CategoryItem> categoryItemsIterator = categoryItemsTTRSSResponse.getContent().iterator();\n            CategoryItem TTRSSCategoryItem;\n            ArrayList<Category> categories = new ArrayList<>();\n            while (categoryItemsIterator.hasNext()) {\n                TTRSSCategoryItem = categoryItemsIterator.next();\n                if (Integer.parseInt(TTRSSCategoryItem.getId()) < 1) {\n                    continue;\n                }\n                categories.add(TTRSSCategoryItem.convert());\n            }\n\n            // 获取feed\n            TinyResponse<List<FeedItem>> feedItemsTTRSSResponse = service.getFeeds(getAuthorization(),new GetFeeds(getAuthorization())).execute().body();\n            if (!feedItemsTTRSSResponse.isSuccessful()) {\n                throw new HttpException(\"获取失败\");\n            }\n\n            Iterator<FeedItem> feedItemsIterator = feedItemsTTRSSResponse.getContent().iterator();\n            FeedItem ttrssFeedItem;\n            ArrayList<Feed> feeds = new ArrayList<>();\n            ArrayList<FeedCategory> feedCategories = new ArrayList<>(feedItemsTTRSSResponse.getContent().size());\n            FeedCategory feedCategoryTmp;\n            while (feedItemsIterator.hasNext()) {\n                ttrssFeedItem = feedItemsIterator.next();\n                Feed feed = ttrssFeedItem.convert2Feed();\n                feed.setUid(uid);\n                feeds.add(feed);\n                feedCategoryTmp = new FeedCategory(uid, String.valueOf(ttrssFeedItem.getId()), String.valueOf(ttrssFeedItem.getCatId()));\n                feedCategories.add(feedCategoryTmp);\n            }\n\n            // 如果在获取到数据的时候就保存，那么到这里同步断了的话，可能系统内的文章就找不到响应的分组，所有放到这里保存。\n            // 覆盖保存，只会保留最新一份。（比如在云端将文章移到的新的分组）\n            coverSaveFeeds(feeds);\n            coverSaveCategories(categories);\n            coverFeedCategory(feedCategories);\n\n            // 获取所有未读的资源\n            KLog.e(\"2 - 同步文章信息\");\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post(getString(R.string.sync_article_refs));\n\n            GetHeadlines getHeadlines = new GetHeadlines();\n            getHeadlines.setSid(getAuthorization());\n            Article article = CoreDB.i().articleDao().getLastArticle(uid);\n            if (null != article) {\n                getHeadlines.setSince_id(article.getId());\n            }\n\n\n            TinyResponse<String> idsResponse;\n            // 获取未读资源\n            idsResponse = service.getUnreadItemIds(getAuthorization(),new GetUnreadItemIds(getAuthorization())).execute().body();\n            if (!idsResponse.isSuccessful()) {\n                throw new HttpException(\"获取失败\");\n            }\n            KLog.e(\"未读\" + idsResponse.getContent());\n            HashSet<String> unreadRefsSet = handleUnreadRefs( idsResponse.getContent().split(\",\") );\n\n            // 获取加星资源\n            GetSavedItemIds getSavedItemIds = new GetSavedItemIds(getAuthorization());\n            idsResponse = service.getSavedItemIds(getAuthorization(),getSavedItemIds).execute().body();\n            if (!idsResponse.isSuccessful()) {\n                throw new HttpException(\"获取失败\");\n            }\n            HashSet<String> staredRefsSet = handleStaredRefs( idsResponse.getContent().split(\",\") );\n\n\n            HashSet<String> idRefsSet = new HashSet<>();\n            idRefsSet.addAll(unreadRefsSet);\n            idRefsSet.addAll(staredRefsSet);\n\n            KLog.i(\"文章id资源：\" + idRefsSet );\n            ArrayList<String> ids = new ArrayList<>(idRefsSet);\n\n            int hadFetchCount, needFetchCount, num;\n            ArrayMap<String, ArrayList<Article>> classArticlesMap = new ArrayMap<String, ArrayList<Article>>();\n\n            needFetchCount = ids.size();\n            hadFetchCount = 0;\n\n            GetArticles getArticles = new GetArticles(getAuthorization());\n            KLog.e(\"1 - 同步文章内容\" + needFetchCount + \"   \" );\n\n            TinyResponse<List<ArticleItem>> ttrssArticleItemsResponse;\n            ArrayList<Article> articles;\n\n            while (needFetchCount > 0) {\n                num = Math.min(needFetchCount, fetchContentCntForEach);\n                getArticles.setArticleIds( ids.subList(hadFetchCount, hadFetchCount = hadFetchCount + num) );\n                ttrssArticleItemsResponse = service.getArticles(getAuthorization(),getArticles).execute().body();\n                if (!ttrssArticleItemsResponse.isSuccessful()) {\n                    throw new HttpException(\"获取失败\");\n                }\n                List<ArticleItem> items = ttrssArticleItemsResponse.getContent();\n                articles = new ArrayList<>(items.size());\n                long syncTimeMillis = System.currentTimeMillis();\n                for (ArticleItem item : items) {\n                    articles.add(item.convert(new ArticleChanger() {\n                        @Override\n                        public Article change(Article article) {\n                            article.setCrawlDate(syncTimeMillis);\n                            article.setUid(uid);\n                            return article;\n                        }\n                    }));\n                }\n\n                CoreDB.i().articleDao().insert(articles);\n                needFetchCount = ids.size() - hadFetchCount;\n                LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post( App.i().getString(R.string.sync_article_content, hadFetchCount, ids.size()) );\n            }\n\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post(getString(R.string.clear_article));\n            deleteExpiredArticles();\n            handleDuplicateArticle();\n            handleCrawlDate();\n            updateCollectionCount();\n\n            // 获取文章全文\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post(getString(R.string.fetch_article_full_content));\n            fetchReadability(uid, startSyncTimeMillis);\n\n            // 为所有新增的加星文章自动生成tag\n            handleNotTagStarArticles(uid, startSyncTimeMillis);\n            // 执行文章自动处理脚本\n            ArticleActionConfig.i().exeRules(uid,startSyncTimeMillis);\n            // 清理无文章的tag\n            //clearNotArticleTags(uid);\n\n            // 提示更新完成\n            LiveEventBus.get(SyncWorker.NEW_ARTICLE_NUMBER).post(ids.size());\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post( null );\n        }catch (IllegalStateException e){\n            KLog.e(\"同步时产生IllegalStateException：\" + e.getMessage());\n            e.printStackTrace();\n            handleException( \"同步时产生IllegalStateException：\" + e.getMessage() );\n        }catch (HttpException e) {\n            KLog.e(\"同步时产生HttpException：\" + e.message());\n            e.printStackTrace();\n            handleException(\"同步时产生HttpException：\" + e.message());\n        } catch (ConnectException e) {\n            KLog.e(\"同步时产生异常ConnectException\");\n            e.printStackTrace();\n            handleException(\"同步时产生异常ConnectException\");\n        } catch (SocketTimeoutException e) {\n            KLog.e(\"同步时产生异常SocketTimeoutException\");\n            e.printStackTrace();\n            handleException(\"同步时产生异常SocketTimeoutException\");\n        } catch (IOException e) {\n            KLog.e(\"同步时产生异常IOException\");\n            e.printStackTrace();\n            handleException(\"同步时产生异常IOException\");\n        } catch (RuntimeException e) {\n            KLog.e(\"同步时产生异常RuntimeException\");\n            e.printStackTrace();\n            handleException(\"同步时产生异常RuntimeException\");\n        }\n    }\n\n    private void handleException(String msg) {\n        ToastUtils.show(msg);\n        LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post( null );\n    }\n\n    @Override\n    public void renameTag(String tagId, String targetName, CallbackX cb) {\n        cb.onFailure(\"暂时不支持\");\n    }\n\n    public void addFeed(EditFeed editFeed, CallbackX cb) {\n        SubscribeToFeed subscribeToFeed = new SubscribeToFeed(getAuthorization());\n        subscribeToFeed.setFeed_url(editFeed.getId());\n        if (editFeed.getCategoryItems() != null && editFeed.getCategoryItems().size() != 0) {\n            subscribeToFeed.setCategory_id(editFeed.getCategoryItems().get(0).getId());\n        }\n        service.subscribeToFeed(getAuthorization(),subscribeToFeed).enqueue(new retrofit2.Callback<TinyResponse<SubscribeToFeedResult>>() {\n            @Override\n            public void onResponse(@NotNull retrofit2.Call<TinyResponse<SubscribeToFeedResult>> call, @NotNull Response<TinyResponse<SubscribeToFeedResult>> response) {\n                if (response.isSuccessful() && response.body().isSuccessful()) {\n                    KLog.e(\"添加成功\" + response.body().toString());\n                    cb.onSuccess(\"添加成功\");\n                } else {\n                    cb.onFailure(\"响应失败\");\n                }\n            }\n\n            @Override\n            public void onFailure(@NotNull retrofit2.Call<TinyResponse<SubscribeToFeedResult>> call, @NotNull Throwable t) {\n                cb.onFailure(\"添加失败\");\n                KLog.e(\"添加失败\");\n            }\n        });\n    }\n\n    @Override\n    public void renameFeed(String feedId, String renamedTitle, CallbackX cb) {\n        cb.onFailure(\"暂时不支持\");\n    }\n\n    /**\n     * 订阅，编辑feed\n     *\n     * @param feedId\n     * @param feedTitle\n     * @param categoryItems\n     * @param cb\n     */\n    public void editFeed(@NonNull String feedId, @Nullable String feedTitle, @Nullable ArrayList<me.wizos.loread.bean.feedly.CategoryItem> categoryItems, StringCallback cb) {\n    }\n\n\n    @Override\n    public void editFeedCategories(List<me.wizos.loread.bean.feedly.CategoryItem> lastCategoryItems, EditFeed editFeed, CallbackX cb) {\n        cb.onFailure(\"暂时不支持\");\n    }\n\n    public void unsubscribeFeed(String feedId,CallbackX cb) {\n        UnsubscribeFeed unsubscribeFeed = new UnsubscribeFeed(getAuthorization());\n        unsubscribeFeed.setFeedId(Integer.parseInt(feedId));\n        service.unsubscribeFeed(getAuthorization(),unsubscribeFeed).enqueue(new retrofit2.Callback<TinyResponse<Map>>() {\n            @Override\n            public void onResponse(@NotNull retrofit2.Call<TinyResponse<Map>> call, @NotNull Response<TinyResponse<Map>> response) {\n                if(response.isSuccessful() && null != response.body() && null != response.body().getContent() && \"OK\".equals(response.body().getContent().get(\"status\"))){\n                    cb.onSuccess(\"退订成功\");\n                }else {\n                    cb.onFailure(\"退订失败\" + response.body());\n                }\n            }\n\n            @Override\n            public void onFailure(@NotNull retrofit2.Call<TinyResponse<Map>> call, @NotNull Throwable t) {\n                cb.onSuccess(\"退订失败\" + t.getMessage());\n            }\n        });\n    }\n\n\n    private void markArticles(int field, int mode, List<String> ids,CallbackX cb) {\n        UpdateArticle updateArticle = new UpdateArticle(getAuthorization());\n        updateArticle.setArticle_ids(StringUtils.join(\",\", ids));\n        updateArticle.setField(field);\n        updateArticle.setMode(mode);\n        service.updateArticle(getAuthorization(),updateArticle).enqueue(new retrofit2.Callback<TinyResponse<UpdateArticleResult>>() {\n            @Override\n            public void onResponse(@NotNull retrofit2.Call<TinyResponse<UpdateArticleResult>> call, @NotNull Response<TinyResponse<UpdateArticleResult>> response) {\n                if (response.isSuccessful() ){\n                    cb.onSuccess(null);\n                }else {\n                    cb.onFailure(\"修改失败，原因未知\");\n                }\n            }\n\n            @Override\n            public void onFailure(@NotNull retrofit2.Call<TinyResponse<UpdateArticleResult>> call, @NotNull Throwable t) {\n                cb.onFailure(\"修改失败，原因未知\");\n            }\n        });\n    }\n\n    private void markArticle(int field, int mode, String articleId,CallbackX cb) {\n        UpdateArticle updateArticle = new UpdateArticle(getAuthorization());\n        updateArticle.setArticle_ids(articleId);\n        updateArticle.setField(field);\n        updateArticle.setMode(mode);\n        service.updateArticle(getAuthorization(),updateArticle).enqueue(new retrofit2.Callback<TinyResponse<UpdateArticleResult>>() {\n            @Override\n            public void onResponse(@NotNull retrofit2.Call<TinyResponse<UpdateArticleResult>> call, @NotNull Response<TinyResponse<UpdateArticleResult>> response) {\n                if (response.isSuccessful() ){\n                    cb.onSuccess(null);\n                }else {\n                    cb.onFailure(\"修改失败，原因未知\");\n                }\n            }\n\n            @Override\n            public void onFailure(@NotNull retrofit2.Call<TinyResponse<UpdateArticleResult>> call, @NotNull Throwable t) {\n                cb.onFailure(\"修改失败，原因未知\");\n            }\n        });\n    }\n\n    public void markArticleListReaded(List<String> articleIds,CallbackX cb) {\n        markArticles(2, 0, articleIds, cb);\n    }\n\n    public void markArticleReaded(String articleId, CallbackX cb) {\n        markArticle(2, 0, articleId, cb);\n    }\n\n    public void markArticleUnread(String articleId, CallbackX cb) {\n        markArticle(2, 1, articleId, cb);\n    }\n\n    public void markArticleStared(String articleId, CallbackX cb) {\n        markArticle(0, 1, articleId, cb);\n    }\n\n    public void markArticleUnstar(String articleId,CallbackX cb) {\n        markArticle(0, 0, articleId, cb);\n    }\n\n    private HashSet<String> handleUnreadRefs(String[] ids) {\n        KLog.i(\"处理未读资源：\" + ids.length );\n        String uid = App.i().getUser().getId();\n        List<Article> localUnreadArticles = CoreDB.i().articleDao().getUnreadNoOrder(uid);\n\n        Map<String, Article> localUnreadArticlesMap = new ArrayMap<>(localUnreadArticles.size());\n        List<Article> changedArticles = new ArrayList<>();\n        // 筛选下来，最终要去云端获取内容的未读Refs的集合\n        HashSet<String> tempUnreadIds = new HashSet<>(ids.length);\n        // 数据量大的一方\n        for (Article article : localUnreadArticles) {\n            localUnreadArticlesMap.put(article.getId(), article);\n        }\n        // 数据量小的一方\n        Article article;\n        for (String articleId : ids) {\n            article = localUnreadArticlesMap.get(articleId);\n            if (article != null) {\n                localUnreadArticlesMap.remove(articleId);\n            } else {\n                article = CoreDB.i().articleDao().getById(uid,articleId);\n                if (article != null && article.getReadStatus() == App.STATUS_READED) {\n                    article.setReadStatus(App.STATUS_UNREAD);\n                    changedArticles.add(article);\n                } else {\n                    // 本地无，而云端有，加入要请求的未读资源\n                    tempUnreadIds.add(articleId+\"\");\n                }\n            }\n        }\n        for (Map.Entry<String, Article> entry : localUnreadArticlesMap.entrySet()) {\n            if (entry.getKey() != null) {\n                article = localUnreadArticlesMap.get(entry.getKey());\n                // 本地未读设为已读\n                article.setReadStatus(App.STATUS_READED);\n                changedArticles.add(article);\n            }\n        }\n\n        CoreDB.i().articleDao().update(changedArticles);\n        return tempUnreadIds;\n    }\n\n    private HashSet<String> handleStaredRefs(String[] ids) {\n        KLog.i(\"处理加薪资源：\" + ids.length);\n        String uid = App.i().getUser().getId();\n        List<Article> localStarredArticles = CoreDB.i().articleDao().getStaredNoOrder(uid);\n\n        Map<String, Article> localStarredArticlesMap = new ArrayMap<>(localStarredArticles.size());\n        List<Article> changedArticles = new ArrayList<>();\n        HashSet<String> tempStarredIds = new HashSet<>(ids.length);\n\n        // 第1步，遍历数据量大的一方A，将其比对项目放入Map中\n        for (Article article : localStarredArticles) {\n            localStarredArticlesMap.put(article.getId(), article);\n        }\n\n        // 第2步，遍历数据量小的一方B。到Map中找，是否含有b中的比对项。有则XX，无则YY\n        Article article;\n        for (String articleId : ids) {\n            article = localStarredArticlesMap.get(articleId);\n            if (article != null) {\n                localStarredArticlesMap.remove(articleId);\n            } else {\n                article = CoreDB.i().articleDao().getById(uid,articleId);\n                if (article != null) {\n                    article.setStarStatus(App.STATUS_STARED);\n                    changedArticles.add(article);\n                } else {\n                    // 本地无，而云远端有，加入要请求的未读资源\n                    tempStarredIds.add(articleId);\n                }\n            }\n        }\n\n        for (Map.Entry<String, Article> entry : localStarredArticlesMap.entrySet()) {\n            if (entry.getKey() != null) {\n                article = localStarredArticlesMap.get(entry.getKey());\n                article.setStarStatus(App.STATUS_UNSTAR);\n                changedArticles.add(article);// 取消加星\n            }\n        }\n\n        CoreDB.i().articleDao().update(changedArticles);\n        return tempStarredIds;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/api/LoreadService.java",
    "content": "package me.wizos.loread.network.api;\n\nimport androidx.annotation.NonNull;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport me.wizos.loread.bean.ttrss.request.GetArticles;\nimport me.wizos.loread.bean.ttrss.request.GetCategories;\nimport me.wizos.loread.bean.ttrss.request.GetFeeds;\nimport me.wizos.loread.bean.ttrss.request.GetHeadlines;\nimport me.wizos.loread.bean.ttrss.request.GetSavedItemIds;\nimport me.wizos.loread.bean.ttrss.request.GetUnreadItemIds;\nimport me.wizos.loread.bean.ttrss.request.LoginParam;\nimport me.wizos.loread.bean.ttrss.request.SubscribeToFeed;\nimport me.wizos.loread.bean.ttrss.request.UnsubscribeFeed;\nimport me.wizos.loread.bean.ttrss.request.UpdateArticle;\nimport me.wizos.loread.bean.ttrss.result.ArticleItem;\nimport me.wizos.loread.bean.ttrss.result.CategoryItem;\nimport me.wizos.loread.bean.ttrss.result.FeedItem;\nimport me.wizos.loread.bean.ttrss.result.SubscribeToFeedResult;\nimport me.wizos.loread.bean.ttrss.result.TTRSSLoginResult;\nimport me.wizos.loread.bean.ttrss.result.TinyResponse;\nimport me.wizos.loread.bean.ttrss.result.UpdateArticleResult;\nimport retrofit2.Call;\nimport retrofit2.http.Body;\nimport retrofit2.http.Header;\nimport retrofit2.http.Headers;\nimport retrofit2.http.POST;\n\n/**\n * Created by Wizos on 2019/11/23.\n */\n\npublic interface LoreadService {\n    // Post请求的文本参数则用注解@Field来声明，同时还必须给方法添加注解@FormUrlEncoded来告知Retrofit参数为表单参数，如果只为参数增加@Field注解，而不给方法添加@FormUrlEncoded注解运行时会抛异常。\n    @Headers(\"Accept: application/json\")\n    @POST(\"plugins.local/loread/\")\n    Call<TinyResponse<TTRSSLoginResult>> login(\n            @NonNull @Body LoginParam loginParam\n    );\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"plugins.local/loread/\")\n    Call<TinyResponse<List<CategoryItem>>> getCategories(\n            @Header(\"authorization\") String authorization,\n            @NonNull @Body GetCategories getCategories\n    );\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"plugins.local/loread/\")\n    Call<TinyResponse<List<FeedItem>>> getFeeds(\n            @Header(\"authorization\") String authorization,\n            @NonNull @Body GetFeeds getFeeds\n    );\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"plugins.local/loread/\")\n    Call<TinyResponse<String>> getUnreadItemIds(\n            @Header(\"authorization\") String authorization,\n            @NonNull @Body GetUnreadItemIds getUnreadItemIds\n    );\n    @Headers(\"Accept: application/json\")\n    @POST(\"plugins.local/loread/\")\n    Call<TinyResponse<String>> getSavedItemIds(\n            @Header(\"authorization\") String authorization,\n            @NonNull @Body GetSavedItemIds getSavedItemIds\n    );\n    @Headers(\"Accept: application/json\")\n    @POST(\"plugins.local/loread/\")\n    Call<TinyResponse<List<ArticleItem>>> getHeadlines(\n            @Header(\"authorization\") String authorization,\n            @NonNull @Body GetHeadlines getHeadlines\n    );\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"plugins.local/loread/\")\n    Call<TinyResponse<List<ArticleItem>>> getArticles(\n            @Header(\"authorization\") String authorization,\n            @NonNull @Body GetArticles getArticles\n    );\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"plugins.local/loread/\")\n    Call<TinyResponse<UpdateArticleResult>> updateArticle(\n            @Header(\"authorization\") String authorization,\n            @NonNull @Body UpdateArticle updateArticle\n    );\n\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"plugins.local/loread/\")\n    Call<TinyResponse<SubscribeToFeedResult>> subscribeToFeed(\n            @Header(\"authorization\") String authorization,\n            @NonNull @Body SubscribeToFeed subscribeToFeed\n    );\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"plugins.local/loread/\")\n    Call<TinyResponse<Map>> unsubscribeFeed(\n            @Header(\"authorization\") String authorization,\n            @NonNull @Body UnsubscribeFeed unsubscribeFeed\n    );\n\n//    @Headers(\"Accept: application/json\")\n//    @POST(\"subscriptions\")\n//    Call<List<EditFeed>> editFeed(\n//            @NonNull @Body EditFeed editFeed\n//    );\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/api/OAuthApi.java",
    "content": "package me.wizos.loread.network.api;\n\nimport java.io.IOException;\n\nimport me.wizos.loread.network.callback.CallbackX;\n\npublic abstract class OAuthApi<T, E> extends AuthApi<T, E> {\n\n    abstract public String getOAuthUrl();\n\n    /**\n     * 获取access_token，refresh_token，expires_in\n     *\n     * @param authorizationCode\n     * @return\n     * @throws IOException\n     */\n    //abstract public String getAccessToken(String authorizationCode) throws IOException;\n    abstract public void getAccessToken(String authorizationCode, CallbackX cb);\n    /**\n     * 刷新access_token，refresh_token，expires_in\n     *\n     * @return\n     * @throws IOException\n     */\n    abstract public String refreshingAccessToken(String refreshToken) throws IOException;\n    abstract public void refreshingAccessToken(String refreshToken, CallbackX cb);\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/api/TinyRSSApi.java",
    "content": "package me.wizos.loread.network.api;\n\nimport android.text.TextUtils;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.collection.ArrayMap;\n\nimport com.google.gson.GsonBuilder;\nimport com.hjq.toast.ToastUtils;\nimport com.jeremyliao.liveeventbus.LiveEventBus;\nimport com.lzy.okgo.callback.StringCallback;\nimport com.lzy.okgo.exception.HttpException;\nimport com.socks.library.KLog;\n\nimport java.io.IOException;\nimport java.net.ConnectException;\nimport java.net.SocketTimeoutException;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.activity.login.LoginResult;\nimport me.wizos.loread.bean.feedly.input.EditFeed;\nimport me.wizos.loread.bean.ttrss.request.GetArticles;\nimport me.wizos.loread.bean.ttrss.request.GetCategories;\nimport me.wizos.loread.bean.ttrss.request.GetFeeds;\nimport me.wizos.loread.bean.ttrss.request.GetHeadlines;\nimport me.wizos.loread.bean.ttrss.request.GetSavedItemIds;\nimport me.wizos.loread.bean.ttrss.request.GetUnreadItemIds;\nimport me.wizos.loread.bean.ttrss.request.LoginParam;\nimport me.wizos.loread.bean.ttrss.request.SubscribeToFeed;\nimport me.wizos.loread.bean.ttrss.request.UnsubscribeFeed;\nimport me.wizos.loread.bean.ttrss.request.UpdateArticle;\nimport me.wizos.loread.bean.ttrss.result.ArticleItem;\nimport me.wizos.loread.bean.ttrss.result.CategoryItem;\nimport me.wizos.loread.bean.ttrss.result.FeedItem;\nimport me.wizos.loread.bean.ttrss.result.SubscribeToFeedResult;\nimport me.wizos.loread.bean.ttrss.result.TTRSSLoginResult;\nimport me.wizos.loread.bean.ttrss.result.TinyResponse;\nimport me.wizos.loread.bean.ttrss.result.UpdateArticleResult;\nimport me.wizos.loread.config.ArticleActionConfig;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.db.Category;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.Feed;\nimport me.wizos.loread.db.FeedCategory;\nimport me.wizos.loread.network.HttpClientManager;\nimport me.wizos.loread.network.SyncWorker;\nimport me.wizos.loread.network.callback.CallbackX;\nimport me.wizos.loread.utils.StringUtils;\nimport retrofit2.Response;\nimport retrofit2.Retrofit;\nimport retrofit2.converter.gson.GsonConverterFactory;\n\nimport static me.wizos.loread.utils.StringUtils.getString;\n\n/**\n * Created by Wizos on 2019/2/8.\n */\n\npublic class TinyRSSApi extends AuthApi<Feed, me.wizos.loread.bean.feedly.CategoryItem> implements LoginInterface{\n    private TinyRSSService service;\n    private static String OFFICIAL_BASE_URL = \"https://example.com\";\n\n    public TinyRSSApi() {\n        this(App.i().getUser().getHost());\n    }\n\n    public TinyRSSApi(String host) {\n        if (!TextUtils.isEmpty(host)) {\n            TinyRSSApi.OFFICIAL_BASE_URL = host;\n        }else {\n            ToastUtils.show(R.string.empty_host);\n        }\n\n        if (!TinyRSSApi.OFFICIAL_BASE_URL.endsWith(\"/\")) {\n            TinyRSSApi.OFFICIAL_BASE_URL = TinyRSSApi.OFFICIAL_BASE_URL + \"/\";\n        }\n\n        Retrofit retrofit = new Retrofit.Builder()\n                .baseUrl(TinyRSSApi.OFFICIAL_BASE_URL) // 设置网络请求的Url地址, 必须以/结尾\n                .addConverterFactory(GsonConverterFactory.create(new GsonBuilder().setLenient().create()))  // 设置数据解析器\n                .client(HttpClientManager.i().ttrssHttpClient())\n                .build();\n        service = retrofit.create(TinyRSSService.class);\n    }\n\n    public static void setHost(String host) {\n        TinyRSSApi.OFFICIAL_BASE_URL = host;\n        KLog.i(\"HOST 地址：\" + OFFICIAL_BASE_URL );\n    }\n\n    public LoginResult login(String accountId, String accountPd) throws IOException {\n        LoginParam loginParam = new LoginParam();\n        loginParam.setUser(accountId);\n        loginParam.setPassword(accountPd);\n        TinyResponse<TTRSSLoginResult> loginResultTTRSSResponse = service.login(loginParam).execute().body();\n        LoginResult loginResult = new LoginResult();\n        if (loginResultTTRSSResponse.isSuccessful()) {\n            return loginResult.setSuccess(true).setData(loginResultTTRSSResponse.getContent().getSession_id());\n        } else {\n            return loginResult.setSuccess(false).setData(loginResultTTRSSResponse.getContent().getSession_id());\n        }\n    }\n\n    public void login(String accountId, String accountPd,CallbackX cb){\n        LoginParam loginParam = new LoginParam();\n        loginParam.setUser(accountId);\n        loginParam.setPassword(accountPd);\n        service.login(loginParam).enqueue(new retrofit2.Callback<TinyResponse<TTRSSLoginResult>>() {\n            @Override\n            public void onResponse(retrofit2.Call<TinyResponse<TTRSSLoginResult>> call, Response<TinyResponse<TTRSSLoginResult>> response) {\n                if(response.isSuccessful()){\n                    TinyResponse<TTRSSLoginResult> loginResultTTRSSResponse = response.body();\n                    if( loginResultTTRSSResponse.isSuccessful()){\n                        cb.onSuccess(loginResultTTRSSResponse.getContent().getSession_id());\n                        return;\n                    }\n                    cb.onFailure(App.i().getString(R.string.login_failed_reason, loginResultTTRSSResponse.toString()));\n                }else {\n                    cb.onFailure(App.i().getString(R.string.login_failed_reason, response.message()));\n                }\n            }\n\n            @Override\n            public void onFailure(retrofit2.Call<TinyResponse<TTRSSLoginResult>> call, Throwable t) {\n                cb.onFailure(App.i().getString(R.string.login_failed_reason, t.getMessage()));\n            }\n        });\n    }\n\n    public void fetchUserInfo(CallbackX cb){\n        cb.onFailure(App.i().getString(R.string.temporarily_not_supported));\n    }\n    //private long syncTimeMillis;\n\n    @Override\n    public void sync() {\n        try {\n            long startSyncTimeMillis = System.currentTimeMillis();\n            String uid = App.i().getUser().getId();\n\n            KLog.e(\"3 - 同步订阅源信息：获取分类\");\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post(getString(R.string.sync_feed_info));\n\n            // 获取分类\n            TinyResponse<List<CategoryItem>> categoryItemsTTRSSResponse = service.getCategories( new GetCategories(getAuthorization()) ).execute().body();\n            KLog.e(\"获取回应：\" + categoryItemsTTRSSResponse);\n            if (!categoryItemsTTRSSResponse.isSuccessful()) {\n                throw new HttpException(\"获取失败\");\n            }\n\n            Iterator<CategoryItem> categoryItemsIterator = categoryItemsTTRSSResponse.getContent().iterator();\n            CategoryItem TTRSSCategoryItem;\n            ArrayList<Category> categories = new ArrayList<>();\n            while (categoryItemsIterator.hasNext()) {\n                TTRSSCategoryItem = categoryItemsIterator.next();\n                if (Integer.parseInt(TTRSSCategoryItem.getId()) < 1) {\n                    continue;\n                }\n                categories.add(TTRSSCategoryItem.convert());\n            }\n\n            // 获取feed\n            TinyResponse<List<FeedItem>> feedItemsTTRSSResponse = service.getFeeds(new GetFeeds(getAuthorization())).execute().body();\n            if (!feedItemsTTRSSResponse.isSuccessful()) {\n                throw new HttpException(\"获取失败\");\n            }\n\n            Iterator<FeedItem> feedItemsIterator = feedItemsTTRSSResponse.getContent().iterator();\n            FeedItem ttrssFeedItem;\n            ArrayList<Feed> feeds = new ArrayList<>();\n            ArrayList<FeedCategory> feedCategories = new ArrayList<>(feedItemsTTRSSResponse.getContent().size());\n            FeedCategory feedCategoryTmp;\n            while (feedItemsIterator.hasNext()) {\n                ttrssFeedItem = feedItemsIterator.next();\n                Feed feed = ttrssFeedItem.convert2Feed();\n                feed.setUid(uid);\n                feeds.add(feed);\n                feedCategoryTmp = new FeedCategory(uid, String.valueOf(ttrssFeedItem.getId()), String.valueOf(ttrssFeedItem.getCatId()));\n                feedCategories.add(feedCategoryTmp);\n            }\n\n            // 如果在获取到数据的时候就保存，那么到这里同步断了的话，可能系统内的文章就找不到响应的分组，所有放到这里保存。\n            // 覆盖保存，只会保留最新一份。（比如在云端将文章移到的新的分组）\n            coverSaveFeeds(feeds);\n            coverSaveCategories(categories);\n            coverFeedCategory(feedCategories);\n\n            // 获取所有未读的资源\n            KLog.e(\"2 - 同步文章信息\");\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post(getString(R.string.sync_article_refs));\n\n            GetHeadlines getHeadlines = new GetHeadlines();\n            getHeadlines.setSid(getAuthorization());\n            Article article = CoreDB.i().articleDao().getLastArticle(uid);\n            if (null != article) {\n                getHeadlines.setSince_id(article.getId());\n            }\n\n\n            TinyResponse<String> idsResponse;\n            // 获取未读资源\n            idsResponse = service.getUnreadItemIds( new GetUnreadItemIds(getAuthorization()) ).execute().body();\n            assert idsResponse != null;\n            if (!idsResponse.isSuccessful()) {\n                throw new HttpException(\"获取失败\");\n            }\n            KLog.e(\"未读\" + idsResponse.getContent());\n            HashSet<String> unreadRefsSet = handleUnreadRefs( idsResponse.getContent().split(\",\") );\n\n            // 获取加星资源\n            idsResponse = service.getSavedItemIds(new GetSavedItemIds(getAuthorization())).execute().body();\n            assert idsResponse != null;\n            if (!idsResponse.isSuccessful()) {\n                throw new HttpException(\"获取失败\");\n            }\n            HashSet<String> staredRefsSet = handleStaredRefs( idsResponse.getContent().split(\",\") );\n\n\n            HashSet<String> idRefsSet = new HashSet<>();\n            idRefsSet.addAll(unreadRefsSet);\n            idRefsSet.addAll(staredRefsSet);\n\n            KLog.i(\"文章id资源：\" + idRefsSet );\n            ArrayList<String> ids = new ArrayList<>(idRefsSet);\n\n            int hadFetchCount, needFetchCount, num;\n            //ArrayMap<String, ArrayList<Article>> classArticlesMap = new ArrayMap<String, ArrayList<Article>>();\n\n            needFetchCount = ids.size();\n            hadFetchCount = 0;\n\n            GetArticles getArticles = new GetArticles(getAuthorization());\n            KLog.e(\"1 - 同步文章内容\" + needFetchCount + \"   \" );\n\n            TinyResponse<List<ArticleItem>> ttrssArticleItemsResponse;\n            ArrayList<Article> articles;\n\n            while (needFetchCount > 0) {\n                num = Math.min(needFetchCount, fetchContentCntForEach);\n                getArticles.setArticleIds( ids.subList(hadFetchCount, hadFetchCount = hadFetchCount + num) );\n                ttrssArticleItemsResponse = service.getArticles(getArticles).execute().body();\n                if (!ttrssArticleItemsResponse.isSuccessful()) {\n                    throw new HttpException(\"获取失败\");\n                }\n                List<ArticleItem> items = ttrssArticleItemsResponse.getContent();\n                articles = new ArrayList<>(items.size());\n                long syncTimeMillis = System.currentTimeMillis();\n                for (ArticleItem item : items) {\n                    articles.add(item.convert(new ArticleChanger() {\n                        @Override\n                        public Article change(Article article) {\n                            article.setCrawlDate(syncTimeMillis);\n                            article.setUid(uid);\n                            return article;\n                        }\n                    }));\n                }\n\n                CoreDB.i().articleDao().insert(articles);\n                needFetchCount = ids.size() - hadFetchCount;\n                LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post( App.i().getString(R.string.sync_article_content, hadFetchCount, ids.size()) );\n            }\n\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post(getString(R.string.clear_article));\n            deleteExpiredArticles();\n            handleDuplicateArticle();\n            handleCrawlDate();\n            updateCollectionCount();\n\n            // 获取文章全文\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post(getString(R.string.fetch_article_full_content));\n            fetchReadability(uid, startSyncTimeMillis);\n\n            // 为所有新增的加星文章自动生成tag\n            handleNotTagStarArticles(uid, startSyncTimeMillis);\n            // 执行文章自动处理脚本\n            ArticleActionConfig.i().exeRules(uid,startSyncTimeMillis);\n            // 清理无文章的tag\n            //clearNotArticleTags(uid);\n\n            // 提示更新完成\n            LiveEventBus.get(SyncWorker.NEW_ARTICLE_NUMBER).post(ids.size());\n            LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post( null );\n        }catch (IllegalStateException e){\n            handleException(e, \"同步失败：IllegalState异常 \"  + e.getMessage());\n        }catch (HttpException e) {\n            handleException(e, \"同步失败：Http异常 \"  + e.message());\n        } catch (ConnectException e) {\n            handleException(e, \"同步失败：Connect异常\");\n        } catch (SocketTimeoutException e) {\n            handleException(e, \"同步失败：Socket超时\");\n        } catch (IOException e) {\n            handleException(e, \"同步失败：IO异常\");\n        } catch (RuntimeException e) {\n            handleException(e, \"同步失败：Runtime异常\");\n        }\n    }\n\n    private void handleException(Exception e, String msg) {\n        KLog.e(msg);\n        ToastUtils.show(msg);\n        LiveEventBus.get(SyncWorker.SYNC_PROCESS_FOR_SUBTITLE).post( null );\n    }\n\n    @Override\n    public void renameTag(String tagId, String targetName, CallbackX cb) {\n        cb.onFailure(\"暂时不支持\");\n    }\n\n    public void addFeed(EditFeed editFeed, CallbackX cb) {\n        SubscribeToFeed subscribeToFeed = new SubscribeToFeed(getAuthorization());\n        subscribeToFeed.setFeed_url(editFeed.getId());\n        if (editFeed.getCategoryItems() != null && editFeed.getCategoryItems().size() != 0) {\n            subscribeToFeed.setCategory_id(editFeed.getCategoryItems().get(0).getId());\n        }\n        service.subscribeToFeed(subscribeToFeed).enqueue(new retrofit2.Callback<TinyResponse<SubscribeToFeedResult>>() {\n            @Override\n            public void onResponse(retrofit2.Call<TinyResponse<SubscribeToFeedResult>> call, Response<TinyResponse<SubscribeToFeedResult>> response) {\n                if (response.isSuccessful() && response.body().isSuccessful()) {\n                    KLog.e(\"添加成功\" + response.body().toString());\n                    cb.onSuccess(\"添加成功\");\n                } else {\n                    cb.onFailure(\"响应失败\");\n                }\n            }\n\n            @Override\n            public void onFailure(retrofit2.Call<TinyResponse<SubscribeToFeedResult>> call, Throwable t) {\n                cb.onFailure(\"添加失败\");\n                KLog.e(\"添加失败\");\n            }\n        });\n    }\n\n    @Override\n    public void renameFeed(String feedId, String renamedTitle, CallbackX cb) {\n        cb.onFailure(\"暂时不支持\");\n    }\n\n    /**\n     * 订阅，编辑feed\n     *\n     * @param feedId\n     * @param feedTitle\n     * @param categoryItems\n     * @param cb\n     */\n    public void editFeed(@NonNull String feedId, @Nullable String feedTitle, @Nullable ArrayList<me.wizos.loread.bean.feedly.CategoryItem> categoryItems, StringCallback cb) {\n    }\n\n\n    @Override\n    public void editFeedCategories(List<me.wizos.loread.bean.feedly.CategoryItem> lastCategoryItems, EditFeed editFeed, CallbackX cb) {\n        cb.onFailure(\"暂时不支持\");\n    }\n\n    public void unsubscribeFeed(String feedId,CallbackX cb) {\n        UnsubscribeFeed unsubscribeFeed = new UnsubscribeFeed(getAuthorization());\n        unsubscribeFeed.setFeedId(Integer.parseInt(feedId));\n        service.unsubscribeFeed(unsubscribeFeed).enqueue(new retrofit2.Callback<TinyResponse<Map>>() {\n            @Override\n            public void onResponse(retrofit2.Call<TinyResponse<Map>> call, Response<TinyResponse<Map>> response) {\n                if(response.isSuccessful() && null != response.body() && null != response.body().getContent() && \"OK\".equals(response.body().getContent().get(\"status\"))){\n                    if(cb!=null){\n                        cb.onSuccess(null);\n                    }\n                }else {\n                    cb.onFailure(response.body());\n                }\n            }\n\n            @Override\n            public void onFailure(retrofit2.Call<TinyResponse<Map>> call, Throwable t) {\n                cb.onFailure(t.getMessage());\n            }\n        });\n    }\n\n\n    private void markArticles(int field, int mode, List<String> ids,CallbackX cb) {\n        UpdateArticle updateArticle = new UpdateArticle(getAuthorization());\n        updateArticle.setArticle_ids(StringUtils.join(\",\", ids));\n        updateArticle.setField(field);\n        updateArticle.setMode(mode);\n        service.updateArticle(updateArticle).enqueue(new retrofit2.Callback<TinyResponse<UpdateArticleResult>>() {\n            @Override\n            public void onResponse(retrofit2.Call<TinyResponse<UpdateArticleResult>> call, Response<TinyResponse<UpdateArticleResult>> response) {\n                if (response.isSuccessful() ){\n                    if(cb!=null){\n                        cb.onSuccess(null);\n                    }\n                }else {\n                    if(cb!=null){\n                        cb.onFailure(\"修改失败，原因未知\");\n                    }\n                }\n            }\n\n            @Override\n            public void onFailure(retrofit2.Call<TinyResponse<UpdateArticleResult>> call, Throwable t) {\n                if(cb!=null){\n                    cb.onFailure(\"修改失败，原因未知\");\n                }\n            }\n        });\n    }\n\n    private void markArticle(int field, int mode, String articleId,CallbackX cb) {\n        UpdateArticle updateArticle = new UpdateArticle(getAuthorization());\n        updateArticle.setArticle_ids(articleId);\n        updateArticle.setField(field);\n        updateArticle.setMode(mode);\n        service.updateArticle(updateArticle).enqueue(new retrofit2.Callback<TinyResponse<UpdateArticleResult>>() {\n            @Override\n            public void onResponse(retrofit2.Call<TinyResponse<UpdateArticleResult>> call, Response<TinyResponse<UpdateArticleResult>> response) {\n                if (response.isSuccessful() ){\n                    cb.onSuccess(null);\n                }else {\n                    cb.onFailure(\"修改失败，原因未知\");\n                }\n            }\n\n            @Override\n            public void onFailure(retrofit2.Call<TinyResponse<UpdateArticleResult>> call, Throwable t) {\n                cb.onFailure(\"修改失败，原因未知\");\n            }\n        });\n    }\n\n    public void markArticleListReaded(List<String> articleIds,CallbackX cb) {\n        markArticles(2, 0, articleIds, cb);\n    }\n\n    public void markArticleReaded(String articleId, CallbackX cb) {\n        markArticle(2, 0, articleId, cb);\n    }\n\n    public void markArticleUnread(String articleId, CallbackX cb) {\n        markArticle(2, 1, articleId, cb);\n    }\n\n    public void markArticleStared(String articleId, CallbackX cb) {\n        markArticle(0, 1, articleId, cb);\n    }\n\n    public void markArticleUnstar(String articleId,CallbackX cb) {\n        markArticle(0, 0, articleId, cb);\n    }\n\n    private HashSet<String> handleUnreadRefs(String[] ids) {\n        KLog.i(\"处理未读资源：\" + ids.length );\n        String uid = App.i().getUser().getId();\n        List<Article> localUnreadArticles = CoreDB.i().articleDao().getUnreadNoOrder(uid);\n\n        Map<String, Article> localUnreadArticlesMap = new ArrayMap<>(localUnreadArticles.size());\n        List<Article> changedArticles = new ArrayList<>();\n        // 筛选下来，最终要去云端获取内容的未读Refs的集合\n        HashSet<String> tempUnreadIds = new HashSet<>(ids.length);\n        // 数据量大的一方\n        for (Article article : localUnreadArticles) {\n            localUnreadArticlesMap.put(article.getId(), article);\n        }\n        // 数据量小的一方\n        Article article;\n        for (String articleId : ids) {\n            article = localUnreadArticlesMap.get(articleId);\n            if (article != null) {\n                localUnreadArticlesMap.remove(articleId);\n            } else {\n                article = CoreDB.i().articleDao().getById(uid,articleId);\n                if (article != null && article.getReadStatus() == App.STATUS_READED) {\n                    article.setReadStatus(App.STATUS_UNREAD);\n                    changedArticles.add(article);\n                } else {\n                    // 本地无，而云端有，加入要请求的未读资源\n                    tempUnreadIds.add(articleId+\"\");\n                }\n            }\n        }\n        for (Map.Entry<String, Article> entry : localUnreadArticlesMap.entrySet()) {\n            if (entry.getKey() != null) {\n                article = localUnreadArticlesMap.get(entry.getKey());\n                // 本地未读设为已读\n                article.setReadStatus(App.STATUS_READED);\n                changedArticles.add(article);\n            }\n        }\n\n        CoreDB.i().articleDao().update(changedArticles);\n        return tempUnreadIds;\n    }\n\n    private HashSet<String> handleStaredRefs(String[] ids) {\n        KLog.i(\"处理加薪资源：\" + ids.length);\n        String uid = App.i().getUser().getId();\n        List<Article> localStarredArticles = CoreDB.i().articleDao().getStaredNoOrder(uid);\n\n        Map<String, Article> localStarredArticlesMap = new ArrayMap<>(localStarredArticles.size());\n        List<Article> changedArticles = new ArrayList<>();\n        HashSet<String> tempStarredIds = new HashSet<>(ids.length);\n\n        // 第1步，遍历数据量大的一方A，将其比对项目放入Map中\n        for (Article article : localStarredArticles) {\n            localStarredArticlesMap.put(article.getId(), article);\n        }\n\n        // 第2步，遍历数据量小的一方B。到Map中找，是否含有b中的比对项。有则XX，无则YY\n        Article article;\n        for (String articleId : ids) {\n            article = localStarredArticlesMap.get(articleId);\n            if (article != null) {\n                localStarredArticlesMap.remove(articleId);\n            } else {\n                article = CoreDB.i().articleDao().getById(uid,articleId);\n                if (article != null) {\n                    article.setStarStatus(App.STATUS_STARED);\n                    changedArticles.add(article);\n                } else {\n                    // 本地无，而云远端有，加入要请求的加星资源\n                    tempStarredIds.add(articleId);\n                }\n            }\n        }\n\n        for (Map.Entry<String, Article> entry : localStarredArticlesMap.entrySet()) {\n            if (entry.getKey() != null) {\n                article = localStarredArticlesMap.get(entry.getKey());\n                article.setStarStatus(App.STATUS_UNSTAR);\n                changedArticles.add(article);// 取消加星\n            }\n        }\n\n        CoreDB.i().articleDao().update(changedArticles);\n        return tempStarredIds;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/api/TinyRSSService.java",
    "content": "package me.wizos.loread.network.api;\n\nimport androidx.annotation.NonNull;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport me.wizos.loread.bean.ttrss.request.GetArticles;\nimport me.wizos.loread.bean.ttrss.request.GetCategories;\nimport me.wizos.loread.bean.ttrss.request.GetFeeds;\nimport me.wizos.loread.bean.ttrss.request.GetHeadlines;\nimport me.wizos.loread.bean.ttrss.request.GetSavedItemIds;\nimport me.wizos.loread.bean.ttrss.request.GetUnreadItemIds;\nimport me.wizos.loread.bean.ttrss.request.LoginParam;\nimport me.wizos.loread.bean.ttrss.request.SubscribeToFeed;\nimport me.wizos.loread.bean.ttrss.request.UnsubscribeFeed;\nimport me.wizos.loread.bean.ttrss.request.UpdateArticle;\nimport me.wizos.loread.bean.ttrss.result.ArticleItem;\nimport me.wizos.loread.bean.ttrss.result.CategoryItem;\nimport me.wizos.loread.bean.ttrss.result.FeedItem;\nimport me.wizos.loread.bean.ttrss.result.SubscribeToFeedResult;\nimport me.wizos.loread.bean.ttrss.result.TTRSSLoginResult;\nimport me.wizos.loread.bean.ttrss.result.TinyResponse;\nimport me.wizos.loread.bean.ttrss.result.UpdateArticleResult;\nimport retrofit2.Call;\nimport retrofit2.http.Body;\nimport retrofit2.http.Headers;\nimport retrofit2.http.POST;\n\n/**\n * Created by Wizos on 2019/11/23.\n */\n\npublic interface TinyRSSService {\n    @Headers(\"Accept: application/json\")\n    @POST(\"api/\")\n    Call<TinyResponse<TTRSSLoginResult>> isLoginIn(\n            @NonNull @Body LoginParam loginParam\n    );\n\n    // Post请求的文本参数则用注解@Field来声明，同时还必须给方法添加注解@FormUrlEncoded来告知Retrofit参数为表单参数，如果只为参数增加@Field注解，而不给方法添加@FormUrlEncoded注解运行时会抛异常。\n    @Headers(\"Accept: application/json\")\n    @POST(\"api/\")\n    Call<TinyResponse<TTRSSLoginResult>> login(\n            @NonNull @Body LoginParam loginParam\n    );\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"api/\")\n    Call<TinyResponse<List<CategoryItem>>> getCategories(\n            @NonNull @Body GetCategories getCategories\n    );\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"api/\")\n    Call<TinyResponse<List<FeedItem>>> getFeeds(\n            @NonNull @Body GetFeeds getFeeds\n    );\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"api/\")\n    Call<TinyResponse<String>> getUnreadItemIds(\n            @NonNull @Body GetUnreadItemIds getUnreadItemIds\n    );\n    @Headers(\"Accept: application/json\")\n    @POST(\"api/\")\n    Call<TinyResponse<String>> getSavedItemIds(\n            @NonNull @Body GetSavedItemIds getSavedItemIds\n    );\n    @Headers(\"Accept: application/json\")\n    @POST(\"api/\")\n    Call<TinyResponse<List<ArticleItem>>> getHeadlines(\n            @NonNull @Body GetHeadlines getHeadlines\n    );\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"api/\")\n    Call<TinyResponse<List<ArticleItem>>> getArticles(\n            @NonNull @Body GetArticles getArticles\n    );\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"api/\")\n    Call<TinyResponse<UpdateArticleResult>> updateArticle(\n            @NonNull @Body UpdateArticle updateArticle\n    );\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"api/\")\n    Call<TinyResponse<SubscribeToFeedResult>> subscribeToFeed(\n            @NonNull @Body SubscribeToFeed subscribeToFeed\n    );\n\n    @Headers(\"Accept: application/json\")\n    @POST(\"api/\")\n    Call<TinyResponse<Map>> unsubscribeFeed(\n            @NonNull @Body UnsubscribeFeed unsubscribeFeed\n    );\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/callback/CallbackX.java",
    "content": "package me.wizos.loread.network.callback;\n\n/**\n * Created by Wizos on 2019/11/24.\n */\n\npublic interface CallbackX<T,E> {\n    void onSuccess(T result);\n    void onFailure(E error);\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/glide/OkHttpAppGlideModule.java",
    "content": "package me.wizos.loread.network.glide;\n\nimport android.content.Context;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\n\nimport com.bumptech.glide.Glide;\nimport com.bumptech.glide.GlideBuilder;\nimport com.bumptech.glide.Registry;\nimport com.bumptech.glide.annotation.GlideModule;\nimport com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader;\nimport com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;\nimport com.bumptech.glide.load.model.GlideUrl;\nimport com.bumptech.glide.module.AppGlideModule;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.io.InputStream;\n\nimport me.wizos.loread.network.HttpClientManager;\n\n/**\n * Created by Wizos on 2019/4/21.\n */\n\n@GlideModule\npublic class OkHttpAppGlideModule extends AppGlideModule {\n    @Override\n    public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {\n        registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(HttpClientManager.i().imageHttpClient()));\n    }\n\n    @Override\n    public void applyOptions(@NotNull Context context, GlideBuilder builder) {\n        //int diskCacheSizeBytes = 1024 * 1024 * 100;//  100 MB = 104857600, 1GB = 1073741824\n        builder.setDiskCache(new InternalCacheDiskCacheFactory(context, 1073741824)).setLogLevel(Log.ERROR);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/interceptor/InoreaderHeaderInterceptor.java",
    "content": "package me.wizos.loread.network.interceptor;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.io.IOException;\n\nimport me.wizos.loread.network.api.InoReaderApi;\nimport okhttp3.Interceptor;\nimport okhttp3.Request;\nimport okhttp3.Response;\n\npublic class InoreaderHeaderInterceptor implements Interceptor {\n    @NotNull\n    @Override\n    public Response intercept(Chain chain) throws IOException {\n        Request.Builder builder = chain.request().newBuilder();\n        builder.addHeader(\"AppId\", InoReaderApi.APP_ID);\n        builder.addHeader(\"AppKey\", InoReaderApi.APP_KEY);\n        return chain.proceed(builder.build());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/interceptor/LoggerInterceptor.java",
    "content": "package me.wizos.loread.network.interceptor;\n\nimport android.annotation.SuppressLint;\nimport android.text.TextUtils;\n\nimport androidx.annotation.NonNull;\n\nimport com.socks.library.KLog;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.io.IOException;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\n\nimport me.wizos.loread.BuildConfig;\nimport okhttp3.Interceptor;\nimport okhttp3.MediaType;\nimport okhttp3.Request;\nimport okhttp3.Response;\nimport okhttp3.ResponseBody;\nimport okio.Buffer;\nimport okio.BufferedSource;\n\n/**\n * Created by Wizos on 2019/4/3.\n */\n\npublic class LoggerInterceptor implements Interceptor {\n    @NotNull\n    @SuppressLint(\"DefaultLocale\")\n    @Override\n    public Response intercept(@NonNull Chain chain) throws IOException {\n        // 拦截请求，获取到该次请求的request\n        Request request = chain.request();\n        // 执行本次网络请求操作，返回response信息\n        Response response = chain.proceed(request);\n\n        if (BuildConfig.DEBUG) {\n//            long t1 = System.nanoTime();\n//            KLog.i(String.format(\"Sending request %s on %s%n%s\", request.url(), chain.connection(), request.headers()));\n//            long t2 = System.nanoTime();\n//            KLog.i(String.format(\"Received response for %s in %.1fms%n%s\", response.request().url(), (t2 - t1) / 1e6d, response.headers()));\n//\n//\n//            if (!HttpHeaders.hasBody(response)) {\n//                return response;\n//            }\n//\n            ResponseBody responseBody = response.body();\n            if (responseBody == null) {\n                return response;\n            }\n            BufferedSource source = responseBody.source();\n            source.request(Long.MAX_VALUE); // Buffer the entire body.\n            Buffer buffer = source.buffer();\n            Charset charset;\n            MediaType contentType = responseBody.contentType();\n            if (contentType != null) {\n                charset = contentType.charset(StandardCharsets.UTF_8);\n            }else {\n                charset = StandardCharsets.UTF_8;\n            }\n            String bodyString = buffer.clone().readString(charset);\n            if (!TextUtils.isEmpty(bodyString)) {\n                if (bodyString.length() > 88) {\n                    KLog.e(\"body---------->\" + bodyString.substring(0, 88));\n                } else {\n                    KLog.e(\"body---------->\" + bodyString);\n                }\n            }\n        }\n        return response;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/interceptor/LoreadTokenInterceptor.java",
    "content": "package me.wizos.loread.network.interceptor;\n\nimport android.text.TextUtils;\n\nimport com.socks.library.KLog;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.io.IOException;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.activity.login.LoginResult;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.User;\nimport me.wizos.loread.network.api.LoreadApi;\nimport okhttp3.Interceptor;\nimport okhttp3.MediaType;\nimport okhttp3.Request;\nimport okhttp3.Response;\nimport okhttp3.ResponseBody;\nimport okio.Buffer;\nimport okio.BufferedSource;\n\n/**\n * 自动刷新token的拦截器\n * https://www.jianshu.com/p/62ab11ddacc8\n *\n * @author Wizos\n * @version 1.0\n * @date 2019/4/2\n */\n\npublic class LoreadTokenInterceptor implements Interceptor {\n    @NotNull\n    @Override\n    public Response intercept(Chain chain) throws IOException {\n        Request request = chain.request();\n        Response originalResponse = chain.proceed(request);\n\n        /*通过如下的办法曲线取到请求完成的数据\n         *\n         * 原本想通过  originalResponse.body().string()\n         * 去取到请求完成的数据,但是一直报错,不知道是okhttp的bug还是操作不当\n         *\n         * 然后去看了okhttp的源码,找到了这个曲线方法,取到请求完成的数据后,根据特定的判断条件去判断token过期\n         */\n        ResponseBody responseBody = originalResponse.body();\n        BufferedSource source = responseBody.source();\n        source.request(Long.MAX_VALUE); // Buffer the entire body.\n        Buffer buffer = source.getBuffer(); // .buffer();\n        Charset charset;\n        MediaType contentType = responseBody.contentType();\n        if (contentType != null) {\n            charset = contentType.charset(StandardCharsets.UTF_8);\n        }else {\n            charset = StandardCharsets.UTF_8;\n        }\n        String bodyString = buffer.clone().readString(charset);\n        if (!TextUtils.isEmpty(bodyString)) {\n            if (bodyString.length() > 88) {\n                KLog.i(\"body->\" + bodyString.substring(0, 88));\n            } else {\n                KLog.i(\"body->\" + bodyString);\n            }\n        }\n\n        /***************************************/\n\n        //根据和服务端的约定判断token过期\n        if (bodyString.contains(\"\\\"error\\\":\\\"NOT_LOGGED_IN\")) {\n            User user = App.i().getUser();\n\n            // 通过一个特定的接口获取新的token，此处要用到同步的retrofit请求\n            LoginResult loginResult = ((LoreadApi)App.i().getApi()).login(user.getUserId(),user.getUserPassword());\n            if( loginResult.isSuccess() ){\n                KLog.e(\"TokenInterceptor授权过期：成功重新登录 \" + loginResult.getData() );\n                user.setAuth(loginResult.getData());\n                CoreDB.i().userDao().update(user);\n                App.i().getAuthApi().setAuthorization(user.getAuth());\n                Request.Builder builder = request.newBuilder().header(\"authorization\", user.getAuth());\n                return chain.proceed(builder.build());\n            }\n        }\n        // 否则，只需传递原始响应\n        return originalResponse;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/interceptor/RedirectInterceptor.java",
    "content": "package me.wizos.loread.network.interceptor;\n\nimport android.text.TextUtils;\n\nimport java.io.IOException;\n\nimport me.wizos.loread.config.LinkRewriteConfig;\nimport okhttp3.Interceptor;\nimport okhttp3.Request;\nimport okhttp3.Response;\n\n/**\n * 域名重定向 拦截器\n *\n * @author Wizos\n * @version 1.0\n * @date 2019/4/2\n */\n\npublic class RedirectInterceptor implements Interceptor {\n    @Override\n    public Response intercept(Chain chain) throws IOException {\n        Request request = chain.request();\n        String newUrl = LinkRewriteConfig.i().getRedirectUrl( request.url().toString() );\n        if (!TextUtils.isEmpty(newUrl)) {\n            // 创建一个新请求，并相应地修改它\n            request = request.newBuilder().url(newUrl).build();\n        }\n        return chain.proceed(request);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/interceptor/RefererInterceptor.java",
    "content": "//package me.wizos.loread.network.interceptor;\n//\n//import androidx.annotation.NonNull;\n//\n//import com.socks.library.KLog;\n//\n//import java.io.IOException;\n//\n//import me.wizos.loread.config.RefererRule;\n//import me.wizos.loread.utils.StringUtils;\n//import okhttp3.Interceptor;\n//import okhttp3.Request;\n//import okhttp3.Response;\n//\n///**\n// * Referer 拦截器\n// * 用于给指定网站增加referer\n// *\n// * @author Wizos\n// * @version 1.0\n// * @date 2019/4/2\n// */\n//\n//public class RefererInterceptor implements Interceptor {\n//    @Override\n//    @NonNull\n//    public Response intercept(Chain chain) throws IOException {\n//        Request request = chain.request();\n//        String referer = RefererRule.i().guessRefererByUrl(request.url().toString());\n//        if (!StringUtils.isEmpty(referer)) {\n//            request = request.newBuilder().header(\"referer\", referer).build();\n//        }\n//        KLog.i(\"拦截到的referer：\" + request.url().toString() + \" , \" + referer );\n//        return chain.proceed(request);\n//    }\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/interceptor/RelyInterceptor.java",
    "content": "package me.wizos.loread.network.interceptor;\n\nimport android.text.TextUtils;\nimport android.webkit.CookieManager;\n\nimport androidx.annotation.NonNull;\n\nimport com.socks.library.KLog;\n\nimport java.io.IOException;\n\nimport me.wizos.loread.config.LinkRewriteConfig;\nimport me.wizos.loread.config.NetworkRefererConfig;\nimport me.wizos.loread.config.NetworkUserAgentConfig;\nimport okhttp3.Interceptor;\nimport okhttp3.Request;\nimport okhttp3.Response;\n\n/**\n * 依赖 拦截器\n * 用于给指定网站增加 referer，cookie，ua，重定向等\n *\n * @author Wizos\n * @version 1.0\n * @date 2019/4/2\n */\n\npublic class RelyInterceptor implements Interceptor {\n    @Override\n    @NonNull\n    public Response intercept(Chain chain) throws IOException {\n        Request request = chain.request();\n        Request.Builder builder = request.newBuilder();\n        String url = request.url().toString();\n        boolean hasNew = false;\n        String newUrl = LinkRewriteConfig.i().getRedirectUrl(url).trim();\n        if (!TextUtils.isEmpty(newUrl)) {\n            // 创建一个新请求，并相应地修改它\n            builder.url(newUrl);\n            url = newUrl;\n            hasNew = true;\n        }\n\n        // 使用完整的url或者topPrivateDomain都可以获取到cookie\n        String cookie = CookieManager.getInstance().getCookie(url);\n        if (!TextUtils.isEmpty(cookie)) {\n            builder.header(\"Cookie\", cookie );\n            hasNew = true;\n        }\n\n        String referer = NetworkRefererConfig.i().guessRefererByUrl(url);\n        if (!TextUtils.isEmpty(referer)) {\n            builder.header(\"Referer\", referer );\n            hasNew = true;\n        }\n\n        String ua = NetworkUserAgentConfig.i().guessUserAgentByUrl(url);\n        if (!TextUtils.isEmpty(ua)) {\n            builder.header(\"User-Agent\", ua );\n            hasNew = true;\n        }\n        KLog.i(\"拦截到依赖：\" + url + \" , \" + newUrl + \" , \" + referer + \" =  \" + cookie  + \" =  \"  + ua );\n\n        if(hasNew){\n            return chain.proceed(builder.build());\n        }\n        return chain.proceed(request);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/interceptor/TTRSSTokenInterceptor.java",
    "content": "package me.wizos.loread.network.interceptor;\n\nimport android.text.TextUtils;\n\nimport com.socks.library.KLog;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.io.IOException;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.activity.login.LoginResult;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.User;\nimport me.wizos.loread.network.api.TinyRSSApi;\nimport okhttp3.Interceptor;\nimport okhttp3.MediaType;\nimport okhttp3.Request;\nimport okhttp3.Response;\nimport okhttp3.ResponseBody;\nimport okio.Buffer;\nimport okio.BufferedSource;\n\n/**\n * 自动刷新token的拦截器\n * https://www.jianshu.com/p/62ab11ddacc8\n *\n * @author Wizos\n * @version 1.0\n * @date 2019/4/2\n */\n\npublic class TTRSSTokenInterceptor implements Interceptor {\n    @NotNull\n    @Override\n    public Response intercept(Chain chain) throws IOException {\n        Request request = chain.request();\n        Response originalResponse = chain.proceed(request);\n\n        /*通过如下的办法曲线取到请求完成的数据\n         *\n         * 原本想通过  originalResponse.body().string()\n         * 去取到请求完成的数据,但是一直报错,不知道是okhttp的bug还是操作不当\n         *\n         * 然后去看了okhttp的源码,找到了这个曲线方法,取到请求完成的数据后,根据特定的判断条件去判断token过期\n         */\n        ResponseBody responseBody = originalResponse.body();\n        BufferedSource source = responseBody.source();\n        source.request(Long.MAX_VALUE); // Buffer the entire body.\n        Buffer buffer = source.getBuffer(); // .buffer();\n        Charset charset;\n        MediaType contentType = responseBody.contentType();\n        if (contentType != null) {\n            charset = contentType.charset(StandardCharsets.UTF_8);\n        }else {\n            charset = StandardCharsets.UTF_8;\n        }\n        String bodyString = buffer.clone().readString(charset);\n        if (!TextUtils.isEmpty(bodyString)) {\n            if (bodyString.length() > 88) {\n                KLog.i(\"body->\" + bodyString.substring(0, 88));\n            } else {\n                KLog.i(\"body->\" + bodyString);\n            }\n        }\n\n        //根据和服务端的约定判断token过期\n        if (bodyString.contains(\"\\\"error\\\":\\\"NOT_LOGGED_IN\")) {\n            User user = App.i().getUser();\n\n            // 通过一个特定的接口获取新的token，此处要用到同步的retrofit请求\n            LoginResult loginResult = ((TinyRSSApi)App.i().getApi()).login(user.getUserId(),user.getUserPassword());\n            if( loginResult.isSuccess() ){\n                KLog.e(\"TokenInterceptor授权过期：成功重新登录 \" + loginResult.getData() );\n                user.setAuth(loginResult.getData());\n                CoreDB.i().userDao().update(user);\n                App.i().getAuthApi().setAuthorization(user.getAuth());\n                Request.Builder builder = request.newBuilder().header(\"authorization\", user.getAuth());\n                return chain.proceed(builder.build());\n            }\n        }\n        // 否则，只需传递原始响应\n        return originalResponse;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/interceptor/TokenAuthenticator.java",
    "content": "package me.wizos.loread.network.interceptor;\n\nimport android.text.TextUtils;\n\nimport androidx.annotation.NonNull;\n\nimport com.socks.library.KLog;\n\nimport java.io.IOException;\n\nimport me.wizos.loread.App;\nimport okhttp3.Authenticator;\nimport okhttp3.Request;\nimport okhttp3.Response;\nimport okhttp3.Route;\n\n/**\n * 处理 401 Unauthorized\n * Created by Wizos on 2019/4/2.\n */\n\npublic class TokenAuthenticator implements Authenticator {\n    /**\n     * 通过okhttp提供的Authenticator接口，只有在服务端返回HTTP的状态码为401时，才会使用Authenticator接口，如果服务端设计规范，可以尝试如下方法。\n     * <p>\n     * Feedly 在输入错误的 authorization 会报：\n     * 1.请提供授权码 \"errorMessage\": \"must provide authorization token\"\n     * 2.授权码过期 \"errorMessage\": \"token expired: 1552176000000 (-2044594)\"\n     * <p>\n     * Inoreader 则是返回空\n     *\n     * @param route\n     * @param response\n     * @return\n     * @throws IOException\n     */\n    @Override\n    public Request authenticate(Route route, @NonNull Response response) throws IOException {\n        KLog.e(\"TokenAuthenticator授权过期\");\n        // 重试超过限制则放弃\n        if (responseCount(response) >= 2) {\n            return null;\n        }\n        //取出本地的refreshToken\n        String refreshToken = App.i().getUser().getRefreshToken();\n        KLog.e(\"TokenAuthenticator授权过期：刷新码 \" + refreshToken);\n        if (TextUtils.isEmpty(refreshToken)) {\n            return null;\n        }\n\n        // 通过一个特定的接口获取新的token，此处要用到同步的retrofit请求\n        String authorization = App.i().getOAuthApi().refreshingAccessToken(refreshToken);\n\n        //要用retrofit的同步方式\n//        String newToken = call.execute().body();\n        KLog.e(\"TokenAuthenticator授权过期：授权码 \" + authorization);\n\n        return response.request().newBuilder()\n                .header(\"authorization\", authorization)\n                .build();\n    }\n\n\n    private int responseCount(Response response) {\n        int result = 1;\n        while ((response = response.priorResponse()) != null) {\n            result++;\n        }\n        return result;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/network/interceptor/TokenInterceptor.java",
    "content": "//package me.wizos.loreadx.network.interceptor;\n//\n//import android.text.TextUtils;\n//\n//import com.socks.library.KLog;\n//\n//import java.io.IOException;\n//import java.nio.charset.Charset;\n//\n//import me.wizos.loreadx.App;\n//import okhttp3.Interceptor;\n//import okhttp3.MediaType;\n//import okhttp3.Request;\n//import okhttp3.Response;\n//import okhttp3.ResponseBody;\n//import okio.Buffer;\n//import okio.BufferedSource;\n//\n///**\n// * 自动刷新token的拦截器\n// * https://www.jianshu.com/p/62ab11ddacc8\n// *\n// * @author Wizos\n// * @version 1.0\n// * @date 2019/4/2\n// */\n//\n//public class TokenInterceptor implements Interceptor {\n//    private static final Charset UTF8 = Charset.forName(\"UTF-8\");\n//\n//    @Override\n//    public Response intercept(Chain chain) throws IOException {\n//        Request request = chain.request();\n//        Response originalResponse = chain.proceed(request);\n//\n//        /**通过如下的办法曲线取到请求完成的数据\n//         *\n//         * 原本想通过  originalResponse.body().string()\n//         * 去取到请求完成的数据,但是一直报错,不知道是okhttp的bug还是操作不当\n//         *\n//         * 然后去看了okhttp的源码,找到了这个曲线方法,取到请求完成的数据后,根据特定的判断条件去判断token过期\n//         */\n//        ResponseBody responseBody = originalResponse.body();\n//        BufferedSource source = responseBody.source();\n//        source.request(Long.MAX_VALUE); // Buffer the entire body.\n//        Buffer buffer = source.buffer();\n//        Charset charset = UTF8;\n//        MediaType contentType = responseBody.contentType();\n//        if (contentType != null) {\n//            charset = contentType.charset(UTF8);\n//        }\n//        String bodyString = buffer.clone().readString(charset);\n//\n//        if (!TextUtils.isEmpty(bodyString)) {\n//            if (bodyString.length() < 22) {\n//                KLog.e(\"body---------->\" + bodyString.substring(0, 21));\n//            } else {\n//                KLog.e(\"body---------->\" + bodyString);\n//            }\n//        }\n//\n//        /***************************************/\n//\n//        //根据和服务端的约定判断token过期\n//        if (bodyString.contains(\"token expired\")) {\n//            String refreshToken = App.i().getUser().getRefreshToken();\n//\n//            // 通过一个特定的接口获取新的token，此处要用到同步的retrofit请求\n//            String authorization = App.i().getOAuthApi().refreshingAccessToken(refreshToken);\n//\n//            // 创建一个新请求，并使用新令牌相应地修改它\n//            Request newRequest = request.newBuilder()\n//                    .header(\"authorization\", authorization)\n//                    .build();\n//\n//            // 重试请求\n//            originalResponse.body().close();\n//            KLog.e(\"TokenInterceptor授权过期：刷新码 \" + refreshToken + \"，授权码 \" + authorization);\n//            return chain.proceed(newRequest);\n//        }\n//\n//        // 否则，只需传递原始响应\n//        return originalResponse;\n//    }\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/service/AudioService.java",
    "content": "package me.wizos.loread.service;\n\nimport android.app.Service;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.media.AudioAttributes;\nimport android.media.AudioFocusRequest;\nimport android.media.AudioManager;\nimport android.media.MediaPlayer;\nimport android.os.Binder;\nimport android.os.Build;\nimport android.os.IBinder;\nimport android.speech.tts.TextToSpeech;\nimport android.speech.tts.UtteranceProgressListener;\n\nimport androidx.annotation.Nullable;\n\nimport com.hjq.toast.ToastUtils;\nimport com.socks.library.KLog;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.util.Locale;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.config.TestConfig;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.utils.ArticleUtil;\n\nimport static android.media.AudioAttributes.USAGE_MEDIA;\nimport static android.media.AudioManager.AUDIOFOCUS_GAIN;\n\n/**\n * implements TextToSpeech.OnInitListener , TextToSpeech.OnUtteranceCompletedListener\n * http://mp.weixin.qq.com/s?__biz=MzA3NTYzODYzMg==&mid=2653577446&idx=2&sn=940cfe45f8da91277d1046d90368d440&scene=4#wechat_redirect\n */\n\npublic class AudioService extends Service {\n    private static String TAG = \"TTSService\";\n    private TextToSpeech textToSpeech;\n    private MediaPlayer player;\n    private String title = \"\";\n    private int articleNo;\n    private String utteranceId;\n    private boolean isQueue;\n    private boolean isSpeark = false;\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        KLog.e(TAG, \"onCreate\");\n\n        createTextToSpeech();\n        //这里只执行一次，用于准备播放器\n        createMediaPlayer();\n    }\n\n    @Override\n    public int onStartCommand(Intent intent, int flags, int startId) {\n        KLog.e(TAG, \"onStartCommand\");\n        if (intent != null) {\n            articleNo = intent.getIntExtra(\"articleNo\", 0);\n            isQueue = intent.getBooleanExtra(\"isQueue\",false);\n        }\n        return super.onStartCommand(intent, flags, startId);\n    }\n\n    @Nullable\n    @Override\n    public IBinder onBind(Intent intent) {\n        //当执行完了onCreate后，就会执行onBind把操作歌曲的方法返回\n        KLog.e(TAG, \"onBind\");\n        return new AudioControlBinder();\n    }\n\n\n//    @SuppressLint(\"SdCardPath\")\n    public void speak(){\n        Article article = App.i().articlesAdapter.getItem(articleNo);\n//        Article article = CoreDB.i().articleDao().getById(App.i().getUser().getId(),App.i().articlesAdapter.getArticleId(articleNo));\n        KLog.e(\"准备播放\" + article.getId() + \" , \" + utteranceId + \" , \" + textToSpeech.isSpeaking());\n        if ( textToSpeech.isSpeaking() && article.getId().equalsIgnoreCase(utteranceId) ){\n            return;\n        }\n        utteranceId = article.getId();\n\n        String content = ArticleUtil.getContentForSpeak(article);\n\n        if(TestConfig.i().isTtsFile()){\n            File file = new File(App.i().getExternalCacheDir() + \"/\" + utteranceId + \".wav\");\n            textToSpeech.synthesizeToFile(content,null,file,utteranceId);\n            //textToSpeech.synthesizeToFile(content,null,new File(\"/mnt/sdcard/speak.wav\"),\"test\");\n            textToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() {\n                //这个是开始的时候。是先发声之后才会走这里\n                @Override\n                public void onStart(String utteranceId) {\n                    KLog.e(\"textToSpeech UtteranceProgressListener\", \"开始: \" + file.getAbsolutePath() + \"  \" + file.exists());\n                    if(file.exists()){\n                        playMusic(file);\n                    }\n                }\n                //这个是播报完毕的时候 每一次播报完毕都会走\n                @Override\n                public void onDone(String utteranceId) {\n                    KLog.e(\"textToSpeech UtteranceProgressListener\", \"播放完毕  \" + file.exists());\n                    if(file.exists()){\n//                        file.delete();\n                    }\n                    //playMusic(file.getAbsolutePath());\n                }\n                //错误\n                @Override\n                public void onError(String utteranceId) {\n                    KLog.e(\"textToSpeech UtteranceProgressListener\", \"错误\");\n                }\n            });\n        }else {\n            // textToSpeech.speak(content,TextToSpeech.QUEUE_ADD,null, article.getId());\n            textToSpeech.speak(content,TextToSpeech.QUEUE_FLUSH,null, article.getId());\n            textToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() {\n                //这个是开始的时候。是先发声之后才会走这里\n                @Override\n                public void onStart(String utteranceId) {\n                    KLog.i(\"textToSpeech  onStart\");\n                }\n                //这个是播报完毕的时候 每一次播报完毕都会走\n                @Override\n                public void onDone(String utteranceId) {\n                    if(isQueue){\n                        articleNo++;\n                        speak();\n                    }\n\n                    KLog.e(\"播放完毕\" + isQueue + articleNo );\n                }\n                //错误\n                @Override\n                public void onError(String utteranceId) {\n                    KLog.i(\"textToSpeech  onError: \" + utteranceId);\n                }\n            });\n        }\n    }\n\n\n    public void playMusic(final String playUrl) {\n        KLog.i(TAG,\"播放音乐\");\n        try {\n            if (player == null) {\n                player = createMediaPlayer();\n            } else {\n                player.reset();\n                player.stop();\n            }\n            player.setDataSource(playUrl);\n            //异步准备\n            player.prepareAsync();\n        } catch (IllegalStateException e) {\n            e.printStackTrace();\n            KLog.e(\"设置播放地址失败A\");\n        } catch (IOException e) {\n            e.printStackTrace();\n            KLog.e(\"设置播放地址失败B\");\n        }\n    }\n    public void playMusic(File file) {\n        KLog.i(TAG,\"播放音乐\");\n        try {\n            if (player == null) {\n                player = createMediaPlayer();\n            } else {\n                player.reset();\n                player.stop();\n            }\n            FileInputStream fis = new FileInputStream(file);\n            player.setDataSource(fis.getFD());\n            //异步准备\n            player.prepareAsync();\n        } catch (IllegalStateException e) {\n            e.printStackTrace();\n            KLog.e(\"设置播放地址失败A\");\n        } catch (IOException e) {\n            e.printStackTrace();\n            KLog.e(\"设置播放地址失败B\");\n        }\n    }\n\n//    @Override\n//    public void onUtteranceCompleted(String utteranceId){\n//    }\n\n    //该方法包含关于歌曲的操作\n    public class AudioControlBinder extends Binder {\n        public void setPlayStatusListener(PlayStatusListener playStatusListener) {\n            AudioService.this.playStatusListener = playStatusListener;\n        }\n\n        public AudioService getService() {\n            return AudioService.this;\n        }\n\n        //播放或暂停歌曲\n        public void play() {\n            speak();\n            //player.start();\n            KLog.i(\"服务\", \"播放音乐\");\n        }\n\n        public void pause() {\n            if(textToSpeech !=null){\n                textToSpeech.stop();\n                textToSpeech.shutdown();\n            }\n            KLog.i(\"服务\", \"暂停音乐\");\n        }\n\n//        public boolean isPrepared() {\n//            return prepared;\n//        }\n//\n//        public int getBufferedPercent() {\n//            return bufferedPercent;\n//        }\n//\n        //判断是否处于播放状态\n        public boolean isPlaying() {\n            return textToSpeech.isSpeaking();\n        }\n//\n//        //返回歌曲的长度，单位为毫秒\n//        public int getDuration() {\n//            return player.getDuration();\n//        }\n//\n//        //返回歌曲目前的进度，单位为毫秒\n//        public int getCurrentPosition() {\n//            return player.getCurrentPosition();\n//        }\n//\n//        //设置歌曲播放的进度，单位为毫秒\n//        public void seekTo(int mesc) {\n//            player.seekTo(mesc);\n//        }\n//\n//        public void setSpeed(float speed) {\n//            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n//                if (player.isPlaying()) {\n//                    player.setPlaybackParams(player.getPlaybackParams().setSpeed(speed));\n//                } else {\n//                    player.setPlaybackParams(player.getPlaybackParams().setSpeed(speed));\n//                    player.pause(); // 会自动播放，所以要暂停？\n//                }\n//            }\n//        }\n//\n//        public String getSpeed() {\n//            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n//                return player.getPlaybackParams().getSpeed() + \"\";\n//            }\n//            return getString(R.string.music_speed);\n//        }\n\n        public String getTitle() {\n            return title;\n        }\n    }\n\n\n    private PlayStatusListener playStatusListener;\n    public interface PlayStatusListener {\n        void onPlay();\n        void onPause(); // 例如在被其他音乐播放器抢占了焦点\n        void onEnd();\n        void onError(String cause);\n    }\n\n\n    @Override\n    public void onDestroy() {\n        super.onDestroy();\n        if (textToSpeech != null) {\n            textToSpeech.stop();\n            textToSpeech.shutdown();\n        }\n    }\n\n    private int bufferedPercent = 0;\n\n\n    public void createTextToSpeech(){\n        if (textToSpeech == null){\n            textToSpeech = new TextToSpeech(this, new TextToSpeech.OnInitListener() {\n                @Override\n                public void onInit(int status) {\n                    KLog.e(TAG, \"onInit   \"  + status);\n                    if (status == TextToSpeech.SUCCESS) {\n                        //初始化tts引擎\n                        int result = textToSpeech.setLanguage(Locale.CHINA);\n                        KLog.i(\"初始化\" + result );\n                        //设置参数\n                        // ttsParam();\n                        // TextToSpeech.LANG_MISSING_DATA：表示语言的数据丢失\n                        // TextToSpeech.LANG_NOT_SUPPORTED：不支持\n                        if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {\n                            ToastUtils.show( \"语音包丢失或语音不支持\");\n                        }\n                        speak();\n                    }\n                }\n            });\n        }\n    }\n\n\n    public MediaPlayer createMediaPlayer() {\n        requestAudioFocus();\n        player = new MediaPlayer();\n        player.setAudioStreamType(AudioManager.STREAM_MUSIC);\n        //添加准备好的监听\n        player.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {\n            @Override\n            public void onPrepared(MediaPlayer mediaPlayer) {\n                KLog.e(\"准备好了，开始播放\");\n                //mErrorCount = 0;//清空原来的错误\n                //如果准备好了，就会进行这个方法\n                mediaPlayer.start();\n                if (playStatusListener != null) {\n                    playStatusListener.onPlay();\n                }\n            }\n        });\n        player.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {\n            @Override\n            public void onBufferingUpdate(MediaPlayer arg0, int percent) {\n                bufferedPercent = percent;\n                /* 打印缓冲的百分比, 如果缓冲 */\n                KLog.i(\"缓冲了的百分比 : \" + percent + \" %\");\n            }\n        });\n\n        player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {\n            @Override\n            public void onCompletion(MediaPlayer mp) {\n                if (playStatusListener != null) {\n                    playStatusListener.onEnd();\n                }\n            }\n        });\n\n        player.setOnErrorListener(new MediaPlayer.OnErrorListener() {\n            /**\n             *\n             * @param mp\n             * @param what 发生的错误类型\n             * @param extra 特定于错误的额外代码。通常依赖于实现。\n             * @return 如果方法处理了错误，则为True。如果没有处理错误，则为false。返回false，或者根本没有OnErrorListener，将导致调用OnCompletionListener。\n             */\n            @Override\n            public boolean onError(MediaPlayer mp, int what, int extra) {\n                String whatStr = \"\", extraStr = \"\";\n                boolean error = false;\n                switch (extra) {\n                    case MediaPlayer.MEDIA_ERROR_IO:\n                        extraStr = \"文件流错误\";\n                        error = true;\n                        break;\n                    case MediaPlayer.MEDIA_ERROR_MALFORMED:\n                        extraStr = \"格式不正确\";\n                        error = true;\n                        break;\n                    case MediaPlayer.MEDIA_ERROR_UNSUPPORTED:\n                        extraStr = \" 此文件不支持\";\n                        error = true;\n                        break;\n                    case MediaPlayer.MEDIA_ERROR_TIMED_OUT:\n                        extraStr = \"请求超时\";\n                        error = true;\n                        break;\n                    default:\n                        extraStr = \" extra=(\" + extra + \")\";\n                        break;\n                }\n                switch (what) {\n                    case MediaPlayer.MEDIA_ERROR_UNKNOWN:\n                        error = true;\n                        whatStr = \"未知(waht=\" + what + \")\";\n                        break;\n                    case MediaPlayer.MEDIA_ERROR_SERVER_DIED:\n                        error = true;\n                        whatStr = \"服务器已关闭\";\n                        break;\n                    default:\n                        whatStr = \"(waht:\" + what + \")\";\n                }\n\n                if (playStatusListener != null && error) {\n                    playStatusListener.onError(whatStr + \", \" + extraStr );\n                }\n                KLog.e(\"onError播放出现错误,waht:\" + what + \",extra:\" + extra + \", 原因为：\" + whatStr + \"=\" + extraStr);\n                // 如果方法处理了错误，则为True。如果没有处理，则为false。返回false，或者根本没有OnErrorListener，将导致调用OnCompletionListener。\n                return true;\n            }\n        });\n        return player;\n    }\n\n\n    /**\n     * 好像只能处理一次。\n     * 结果发现 如果另外一个播放器播放获取了焦点了，那么一直就是对方的，除非对方释放了，除非你再次强求也许才会回调 focusChangeListenre，所以\n     * 测试歌曲播放的时候打开qq音乐，然后开始播放 会被qq音乐获取焦点了，然后再在本软件播放然后再用qq音乐打开 无效了，因此 看来 要反复的操作，经不起折腾了，所以视频的我还是直接检测是否在播放播放就关闭了。\n     *\n     */\n    //@RequiresApi(api = Build.VERSION_CODES.O)\n    private void requestAudioFocus() {\n        // 音频管理者，用于处理各app之间的音频冲突\n        AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);\n        assert audioManager != null;\n        if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){\n            AudioFocusRequest audioFocusRequest = new AudioFocusRequest.Builder(AUDIOFOCUS_GAIN)\n                    .setOnAudioFocusChangeListener(afChangeListener)\n                    .setAudioAttributes(new AudioAttributes.Builder().setUsage(USAGE_MEDIA).setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build())\n                    .build();\n            audioManager.requestAudioFocus(audioFocusRequest);\n        }else {\n            audioManager.requestAudioFocus(\n                    // 音频焦点改变监听器\n                    afChangeListener,\n                    // Use the music stream.\n                    AudioManager.STREAM_MUSIC,\n                    // Request permanent focus.\n                    AUDIOFOCUS_GAIN);\n        }\n    }\n\n//    private void abandonAudioFocus() {\n//        audioManager.abandonAudioFocus(null);\n//    }\n\n    private boolean lastAudioFocusIsLossTransient = false;\n    AudioManager.OnAudioFocusChangeListener afChangeListener = new AudioManager.OnAudioFocusChangeListener() {\n        public void onAudioFocusChange(int focusChange) {\n            /*\n             * focusChange主要有以下四种参数：\n             * AUDIOFOCUS_AGIN:你已经完全获得了音频焦点\n             * AUDIOFOCUS_LOSS:你会长时间的失去焦点，所以不要指望在短时间内能获得。请结束自己的相关音频工作并做好收尾工作。比如另外一个音乐播放器开始播放音乐了（前提是这个另外的音乐播放器他也实现了音频焦点的控制，baidu音乐，天天静听很遗憾的就没有实现，所以他们两个是可以跟别的播放器同时播放的）\n             * AUDIOFOCUS_LOSS_TRANSIENT:你会短暂的失去音频焦点，你可以暂停音乐，但不要释放资源，因为你一会就可以夺回焦点并继续使用\n             * AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:你的焦点会短暂失去，但是你可以与新的使用者共同使用音频焦点\n             */\n            KLog.e(\"焦点转移：\" + focusChange);\n            switch (focusChange) {\n                case AUDIOFOCUS_GAIN:\n                    // Resume playback\n                    if (player != null && !player.isPlaying() && lastAudioFocusIsLossTransient) {\n                        player.start();\n                        playStatusListener.onPlay();\n                        lastAudioFocusIsLossTransient = false;\n                    }\n                    break;\n                case AudioManager.AUDIOFOCUS_LOSS:\n                    // audioManager.abandonAudioFocus(afChangeListener);\n                    // Stop playback\n                    if (player != null && player.isPlaying()) {\n                        player.pause();\n                        playStatusListener.onPause();\n                    }\n                    break;\n                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:\n                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:\n                    // Pause playback\n                    if (player != null && player.isPlaying()) {\n                        player.pause();\n                        playStatusListener.onPause();\n                        lastAudioFocusIsLossTransient = true;\n                    }\n                    break;\n            }\n\n        }\n    };\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/service/MainService.java",
    "content": "//package me.wizos.loreadx.service;\n//\n//import android.content.Context;\n//import android.content.Intent;\n//import android.os.AsyncTask;\n//\n//import androidx.annotation.NonNull;\n//import androidx.core.app.JobIntentService;\n//\n//import com.carlt.networklibs.utils.NetworkUtils;\n//import com.socks.library.KLog;\n//\n//import java.util.ArrayList;\n//import java.util.List;\n//\n//import me.wizos.loreadx.App;\n//import me.wizos.loreadx.db.Article;\n//import me.wizos.loreadx.db.CoreDB;\n//import me.wizos.loreadx.utils.EncryptUtil;\n//import me.wizos.loreadx.utils.FileUtil;\n//\n///**\n// * 如果启动 IntentService 多次，那么每一个耗时操作会以工作队列的方式在 IntentService 的 onHandleIntent 回调方法中依次去执行，执行完自动结束。\n// * 这样来避免事务处理阻塞主线程。执行完所一个Intent请求对象所对应的工作之后，如果没有新的Intent请求达到，则自动停止Service；\n// * 否则执行下一个Intent请求所对应的任务。\n// * Created by Wizos on 2019/3/30.\n// */\n//\n//public class MainService extends JobIntentService {\n//    public MainService() {\n//        super();\n//    }\n//\n//    @Override\n//    public void onCreate() {\n//        super.onCreate();\n////        EventBus.getDefault().register(this);\n//    }\n//\n//    @Override\n//    public void onDestroy() {\n//        super.onDestroy();\n////        EventBus.getDefault().unregister(this);\n//    }\n//\n//    /**\n//     * 将工作加入此服务的快捷方法。\n//     */\n//    public static void enqueueWork(Context context, Intent work) {\n//        //KLog.e(\"获取到新的任务：enqueueWork\" );\n//        enqueueWork(context, MainService.class, 100, work);\n//    }\n//\n////    @Subscribe(threadMode = ThreadMode.MAIN)\n////    public void onReceiveesult(Sync sync) {\n////        int result = sync.result;\n//////        KLog.e(\"接收到的数据为：\" + status);\n////        switch (result) {\n////            case Sync.STOP:\n////                stopSelf();\n////                break;\n////            default:\n////                break;\n////        }\n////    }\n//\n//    @Override\n//    protected void onHandleWork(@NonNull Intent intent) {\n//        KLog.e(\"获取到新的任务，线程：\" + Thread.currentThread() );\n//        if (!NetworkUtils.isAvailable()) { //App.i().isSyncing ||\n//            return;\n//        }\n//\n//        AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {\n//            @Override\n//            public void run() {\n//                String action = intent.getAction();\n//                //KLog.e(\"获取到新的任务：\" + action);\n//                if (App.SYNC_ALL.equals(action) || App.SYNC_HEARTBEAT.equals(action)) {\n//                    handleExpiredArticles();\n//                    App.i().getApi().sync();\n//                }\n//            }\n//        });\n//    }\n//\n//    private void handleExpiredArticles() {\n//        // EventBus.getDefault().post(new Sync(Sync.DOING, getString(R.string.clear_article)));\n//\n//        // 最后的 300 * 1000L 是留前5分钟时间的不删除 WithPref.i().getClearBeforeDay()\n//        long time = System.currentTimeMillis() - App.i().getUser().getCachePeriod() * 24 * 3600 * 1000L - 300 * 1000L;\n//        List<Article> boxReadArts = CoreDB.i().articleDao().getReadedUnstarBeFiledLtTime(App.i().getUser().getId(), time);\n//        KLog.i(\"移动文章\" + boxReadArts.size());\n//\n//        for (Article article : boxReadArts) {\n//            article.setSaveStatus(App.STATUS_IS_FILED);\n//            FileUtil.saveArticle(App.i().getUserBoxPath(), article);\n//        }\n//        CoreDB.i().articleDao().update(boxReadArts);\n//\n//        List<Article> storeReadArts = CoreDB.i().articleDao().getReadedStaredBeFiledLtTime(App.i().getUser().getId(), time);\n//        KLog.i(\"移动文章\" + storeReadArts.size());\n//        for (Article article : storeReadArts) {\n//            article.setSaveStatus(App.STATUS_IS_FILED);\n//            FileUtil.saveArticle(App.i().getUserStorePath(), article);\n//        }\n//        CoreDB.i().articleDao().update(storeReadArts);\n//\n//        List<Article> expiredArticles = CoreDB.i().articleDao().getReadedUnstarLtTime(App.i().getUser().getId(), time);\n//        ArrayList<String> idListMD5 = new ArrayList<>(expiredArticles.size());\n//        for (Article article : expiredArticles) {\n//            idListMD5.add(EncryptUtil.MD5(article.getId()));\n//        }\n//        KLog.i(\"清除A：\" + time + \"--\" + expiredArticles.size());\n//        FileUtil.deleteHtmlDirList(idListMD5);\n//        CoreDB.i().articleDao().delete(expiredArticles);\n//    }\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/service/MusicService.java",
    "content": "package me.wizos.loread.service;\n\nimport android.app.Service;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.media.AudioManager;\nimport android.media.MediaPlayer;\nimport android.os.Binder;\nimport android.os.Build;\nimport android.os.IBinder;\nimport android.text.TextUtils;\n\nimport androidx.annotation.Nullable;\n\nimport com.socks.library.KLog;\n\nimport java.io.IOException;\n\nimport me.wizos.loread.R;\n\n/**\n * http://mp.weixin.qq.com/s?__biz=MzA3NTYzODYzMg==&mid=2653577446&idx=2&sn=940cfe45f8da91277d1046d90368d440&scene=4#wechat_redirect\n */\n\npublic class MusicService extends Service {\n    private static String TAG = \"MusicService\";\n    private MediaPlayer player;\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);\n        //这里只执行一次，用于准备播放器\n        player = createMediaPlayer();\n        KLog.e(\"服务\", \"准备播放音乐\");\n    }\n\n    String playUrl;\n    String title = \"\";\n\n    @Override\n    public int onStartCommand(Intent intent, int flags, int startId) {\n        if (intent != null && !TextUtils.isEmpty(intent.getDataString()) && !intent.getDataString().equals(playUrl)) {\n            playUrl = intent.getDataString();\n            KLog.i(\"获取到链接：\" + playUrl);\n            // 补救，获取 playUrl\n            if (TextUtils.isEmpty(playUrl)) {\n                playUrl = intent.getStringExtra(Intent.EXTRA_TEXT);\n            }\n            title = intent.getStringExtra(\"title\");\n            playMusic(playUrl);\n        }\n        return super.onStartCommand(intent, flags, startId);\n    }\n\n    public void playMusic(final String playUrl) {\n        try {\n            if (player == null) {\n                player = createMediaPlayer();\n            } else {\n                player.reset();\n                player.stop();\n            }\n            player.setDataSource(playUrl);\n            //异步准备\n            player.prepareAsync();\n        } catch (IllegalStateException e) {\n            e.printStackTrace();\n            KLog.e(\"设置播放地址失败A\");\n        } catch (IOException e) {\n            e.printStackTrace();\n            KLog.e(\"设置播放地址失败B\");\n        }\n    }\n\n    public void playMusic2(final String playUrl) {\n        try {\n            if (player == null) {\n                player = new MediaPlayer();\n                player.setAudioStreamType(AudioManager.STREAM_MUSIC);\n            } else {\n                KLog.e(\"Player不为空\");\n                player.reset();\n                player.stop();\n            }\n\n            requestAudioFocus();\n\n            player.setDataSource(playUrl);\n            //异步准备\n            player.prepareAsync();\n\n            //添加准备好的监听\n            player.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {\n                @Override\n                public void onPrepared(MediaPlayer mediaPlayer) {\n                    KLog.e(\"准备好了，开始播放\");\n                    //如果准备好了，就会进行这个方法\n                    mediaPlayer.start();\n                    if (playStatusListener != null) {\n                        playStatusListener.onPlay();\n                    }\n                }\n            });\n            player.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {\n                @Override\n                public void onBufferingUpdate(MediaPlayer arg0, int percent) {\n                    bufferedPercent = percent;\n                    /* 打印缓冲的百分比, 如果缓冲 */\n                    KLog.i(\"缓冲了的百分比 : \" + percent + \" %\");\n                }\n            });\n\n            player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {\n                @Override\n                public void onCompletion(MediaPlayer mp) {\n                    if (playStatusListener != null) {\n                        playStatusListener.onEnd();\n                    }\n                }\n            });\n\n            player.setOnErrorListener(new MediaPlayer.OnErrorListener() {\n                /**\n                 *\n                 * @param mp\n                 * @param what 发生的错误类型\n                 * @param extra 特定于错误的额外代码。通常依赖于实现。\n                 * @return 如果方法处理了错误，则为True。如果没有处理错误，则为false。返回false，或者根本没有OnErrorListener，将导致调用OnCompletionListener。\n                 */\n                @Override\n                public boolean onError(MediaPlayer mp, int what, int extra) {\n                    String whatStr = \"\", extraStr = \"\";\n                    boolean error = false;\n                    switch (extra) {\n                        case MediaPlayer.MEDIA_ERROR_IO:\n                            extraStr = \"文件流错误\";\n                            error = true;\n                            break;\n                        case MediaPlayer.MEDIA_ERROR_MALFORMED:\n                            extraStr = \"格式不正确\";\n                            error = true;\n                            break;\n                        case MediaPlayer.MEDIA_ERROR_UNSUPPORTED:\n                            extraStr = \" 此文件不支持\";\n                            error = true;\n                            break;\n                        case MediaPlayer.MEDIA_ERROR_TIMED_OUT:\n                            extraStr = \"请求超时\";\n                            error = true;\n                            break;\n                        default:\n                            extraStr = \" extra=(\" + extra + \")\";\n                            break;\n                    }\n                    switch (what) {\n                        case MediaPlayer.MEDIA_ERROR_UNKNOWN:\n                            error = true;\n                            whatStr = \"未知(waht=\" + what + \")\";\n                            break;\n                        case MediaPlayer.MEDIA_ERROR_SERVER_DIED:\n                            error = true;\n                            whatStr = \"服务器已关闭\";\n                            break;\n                        default:\n                            whatStr = \"(waht:\" + what + \")\";\n                    }\n\n                    if (playStatusListener != null && error) {\n                        playStatusListener.onError(whatStr + \", \" + extraStr );\n                    }\n                    KLog.e(\"onError播放出现错误,waht:\" + what + \",extra:\" + extra + \",\" + whatStr + \"=\" + extraStr);\n                    // 如果方法处理了错误，则为True。如果没有处理，则为false。返回false，或者根本没有OnErrorListener，将导致调用OnCompletionListener。\n                    return true;\n                }\n            });\n        } catch (IllegalStateException e) {\n            e.printStackTrace();\n            KLog.e(\"设置播放地址失败A\");\n        } catch (IOException e) {\n            e.printStackTrace();\n            KLog.e(\"设置播放地址失败B\");\n        }\n    }\n\n\n    private int bufferedPercent = 0;\n    public MediaPlayer createMediaPlayer() {\n        requestAudioFocus();\n        player = new MediaPlayer();\n        player.setAudioStreamType(AudioManager.STREAM_MUSIC);\n        //添加准备好的监听\n        player.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {\n            @Override\n            public void onPrepared(MediaPlayer mediaPlayer) {\n                KLog.e(\"准备好了，开始播放\");\n                //mErrorCount = 0;//清空原来的错误\n                //如果准备好了，就会进行这个方法\n                mediaPlayer.start();\n                if (playStatusListener != null) {\n                    playStatusListener.onPlay();\n                }\n            }\n        });\n        player.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {\n            @Override\n            public void onBufferingUpdate(MediaPlayer arg0, int percent) {\n                bufferedPercent = percent;\n                /* 打印缓冲的百分比, 如果缓冲 */\n                KLog.i(\"缓冲了的百分比 : \" + percent + \" %\");\n            }\n        });\n\n        player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {\n            @Override\n            public void onCompletion(MediaPlayer mp) {\n                if (playStatusListener != null) {\n                    playStatusListener.onEnd();\n                }\n            }\n        });\n\n        player.setOnErrorListener(new MediaPlayer.OnErrorListener() {\n            /**\n             *\n             * @param mp\n             * @param what 发生的错误类型\n             * @param extra 特定于错误的额外代码。通常依赖于实现。\n             * @return 如果方法处理了错误，则为True。如果没有处理错误，则为false。返回false，或者根本没有OnErrorListener，将导致调用OnCompletionListener。\n             */\n            @Override\n            public boolean onError(MediaPlayer mp, int what, int extra) {\n                KLog.e(\"播放出现错误,waht:\" + what + \",extra:\" + extra);\n                String whatStr = \"\";\n                String extraStr = \"\";\n                boolean error = false;\n                switch (extra) {\n                    case MediaPlayer.MEDIA_ERROR_IO:\n                        extraStr = \"文件流错误\";\n                        error = true;\n                        break;\n                    case MediaPlayer.MEDIA_ERROR_MALFORMED:\n                        extraStr = \"格式不正确\";\n                        error = true;\n                        break;\n                    case MediaPlayer.MEDIA_ERROR_UNSUPPORTED:\n                        extraStr = \" 此文件不支持\";\n                        error = true;\n                        break;\n                    case MediaPlayer.MEDIA_ERROR_TIMED_OUT:\n                        extraStr = \"请求超时\";\n                        error = true;\n                        break;\n                    default:\n                        extraStr = \" extra=(\" + extra + \")\";\n                        break;\n                }\n                switch (what) {\n                    case MediaPlayer.MEDIA_ERROR_UNKNOWN:\n                        error = true;\n                        whatStr = \"未知(waht=\" + what + \")\";\n                        break;\n                    case MediaPlayer.MEDIA_ERROR_SERVER_DIED:\n                        error = true;\n                        whatStr = \"服务器已关闭\";\n                        break;\n                    default:\n                        whatStr = \"(waht:\" + what + \")\";\n                }\n\n                if (playStatusListener != null && error) {\n                    playStatusListener.onError(whatStr + \", \" + extraStr );\n                }\n                KLog.e(\"onError播放出现错误,waht:\" + what + \",extra:\" + extra + \",\" + whatStr + \"=\" + extraStr);\n                //mErrorCount = 0;\n                // 如果方法处理了错误，则为True。如果没有处理，则为false。返回false，或者根本没有OnErrorListener，将导致调用OnCompletionListener。\n                return true;\n            }\n        });\n        return player;\n    }\n\n    @Nullable\n    @Override\n    public IBinder onBind(Intent intent) {\n        //当执行完了onCreate后，就会执行onBind把操作歌曲的方法返回\n        return new MusicControlBinder();\n    }\n\n    //该方法包含关于歌曲的操作\n    public class MusicControlBinder extends Binder {\n        public void setPlayStatusListener(PlayStatusListener playStatusListener) {\n            MusicService.this.playStatusListener = playStatusListener;\n        }\n\n        public MusicService getService() {\n            return MusicService.this;\n        }\n\n        //播放或暂停歌曲\n        public void play() {\n            player.start();\n            KLog.i(\"服务\", \"播放音乐\");\n        }\n\n        public void pause() {\n            player.pause();\n            KLog.i(\"服务\", \"暂停音乐\");\n        }\n\n\n        public int getBufferedPercent() {\n            return bufferedPercent;\n        }\n\n        //判断是否处于播放状态\n        public boolean isPlaying() {\n            return player.isPlaying();\n        }\n\n        //返回歌曲的长度，单位为毫秒\n        public int getDuration() {\n            return player.getDuration();\n        }\n\n        //返回歌曲目前的进度，单位为毫秒\n        public int getCurrentPosition() {\n            return player.getCurrentPosition();\n        }\n\n        //设置歌曲播放的进度，单位为毫秒\n        public void seekTo(int mesc) {\n            player.seekTo(mesc);\n        }\n\n        public void setSpeed(float speed) {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n                if (player.isPlaying()) {\n                    player.setPlaybackParams(player.getPlaybackParams().setSpeed(speed));\n                } else {\n                    player.setPlaybackParams(player.getPlaybackParams().setSpeed(speed));\n                    player.pause(); // 会自动播放，所以要暂停？\n                }\n            }\n        }\n\n        public String getSpeed() {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n                return player.getPlaybackParams().getSpeed() + \"\";\n            }\n            return getString(R.string.music_speed);\n        }\n\n        public String getTitle() {\n            return title;\n        }\n    }\n\n    private PlayStatusListener playStatusListener;\n\n    public interface PlayStatusListener {\n        void onPlay();\n        void onPause(); // 例如在被其他音乐播放器抢占了焦点\n        void onEnd();\n        void onError(String cause);\n    }\n\n\n    @Override\n    public void onDestroy() {\n        super.onDestroy();\n        if (player != null) {\n            player.setOnCompletionListener(null);\n            player.setOnPreparedListener(null);\n            player.setOnErrorListener(null);\n            player.reset();\n            player.stop();\n            player.release();\n            player = null;\n        }\n    }\n\n    private boolean lastAudioFocusIsLossTransient = false;\n    private AudioManager audioManager;\n    AudioManager.OnAudioFocusChangeListener afChangeListener = new AudioManager.OnAudioFocusChangeListener() {\n        public void onAudioFocusChange(int focusChange) {\n            /**\n             * focusChange主要有以下四种参数：\n             AUDIOFOCUS_AGIN:你已经完全获得了音频焦点\n             AUDIOFOCUS_LOSS:你会长时间的失去焦点，所以不要指望在短时间内能获得。请结束自己的相关音频工作并做好收尾工作。比如另外一个音乐播放器开始播放音乐了（前提是这个另外的音乐播放器他也实现了音频焦点的控制，baidu音乐，天天静听很遗憾的就没有实现，所以他们两个是可以跟别的播放器同时播放的）\n             AUDIOFOCUS_LOSS_TRANSIENT:你会短暂的失去音频焦点，你可以暂停音乐，但不要释放资源，因为你一会就可以夺回焦点并继续使用\n             AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:你的焦点会短暂失去，但是你可以与新的使用者共同使用音频焦点\n             */\n            KLog.e(\"焦点转移：\" + focusChange);\n            switch (focusChange) {\n                case AudioManager.AUDIOFOCUS_GAIN:\n                    // Resume playback\n                    if (player != null && !player.isPlaying() && lastAudioFocusIsLossTransient) {\n                        player.start();\n                        playStatusListener.onPlay();\n                        lastAudioFocusIsLossTransient = false;\n                    }\n                    break;\n                case AudioManager.AUDIOFOCUS_LOSS:\n                    // audioManager.abandonAudioFocus(afChangeListener);\n                    // Stop playback\n                    if (player != null && player.isPlaying()) {\n                        player.pause();\n                        playStatusListener.onPause();\n                    }\n                    break;\n                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:\n                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:\n                    // Pause playback\n                    if (player != null && player.isPlaying()) {\n                        player.pause();\n                        playStatusListener.onPause();\n                        lastAudioFocusIsLossTransient = true;\n                    }\n                    break;\n            }\n\n        }\n    };\n\n\n    /**\n     * 好像只能处理一次。\n     * 结果发现 如果另外一个播放器播放获取了焦点了，那么一直就是对方的，除非对方释放了，除非你再次强求也许才会回调 focusChangeListenre，所以\n     * 测试歌曲播放的时候打开qq音乐，然后开始播放 会被qq音乐获取焦点了，然后再在本软件播放然后再用qq音乐打开 无效了，因此 看来 要反复的操作，经不起折腾了，所以视频的我还是直接检测是否在播放播放就关闭了。\n     *\n     * @return\n     */\n    private boolean requestAudioFocus() {\n        return AudioManager.AUDIOFOCUS_REQUEST_GRANTED == audioManager.requestAudioFocus(afChangeListener,\n                // Use the music stream.\n                AudioManager.STREAM_MUSIC,\n                // Request permanent focus.\n                AudioManager.AUDIOFOCUS_GAIN);\n    }\n\n    private void abandonAudioFocus() {\n        audioManager.abandonAudioFocus(null);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/service/NetworkStateReceiver.java",
    "content": "package me.wizos.loread.service;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.net.ConnectivityManager;\n\nimport com.socks.library.KLog;\n\nimport me.wizos.loread.utils.NetworkUtil;\n\n\n/**\n * Android 7.0 移除了三项隐式广播，因为隐式广播会在后台频繁启动已注册侦听这些广播的应用。删除这些广播可以显著提升设备性能和用户体验。\n * Android 7.0 以上不会收到 CONNECTIVITY_ACTION 广播，即使它们已有清单条目来请求接受这些事件的通知。\n * 在前台运行的应用如果使用 BroadcastReceiver 请求接收通知，则仍可以在主线程中侦听 CONNECTIVITY_CHANGE。\n * <p>\n * Android 7.0 为了后台优化，推荐使用 JobScheduler 代替 BroadcastReceiver 来监听网络变化。\n *\n * @author Wizos on 2018/6/5.\n */\n\npublic class NetworkStateReceiver extends BroadcastReceiver {\n    @Override\n    public void onReceive(Context context, Intent intent) {\n        KLog.e(\"接收到网络变化\" + intent.getAction() + \" . \" + NetworkUtil.getNetWorkState());\n        if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {\n            NetworkUtil.getNetWorkState();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/ArticleUtil.java",
    "content": "package me.wizos.loread.utils;\n\n\nimport android.text.Html;\n\nimport com.socks.library.KLog;\n\nimport org.jsoup.Jsoup;\nimport org.jsoup.nodes.Document;\nimport org.jsoup.nodes.Element;\nimport org.jsoup.select.Elements;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\nimport me.wizos.loread.bean.Enclosure;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.Feed;\nimport me.wizos.loread.extractor.ExtractorUtil;\nimport okhttp3.MediaType;\nimport okhttp3.ResponseBody;\n\n/**\n * 文章处理工具类\n *\n * @author by Wizos on 2020/3/16.\n */\npublic class ArticleUtil {\n    /**\n     * 将文章保存到内存\n     * @param article\n     * @param title\n     * @return\n     */\n    public static String getPageForSave(Article article, String title) {\n        String published = TimeUtil.format(article.getPubDate(), \"yyyy-MM-dd HH:mm\");\n        String link = article.getLink();\n        String content = getFormatContentForSave(title, article.getContent());\n        Feed feed = CoreDB.i().feedDao().getById(App.i().getUser().getId(), article.getFeedId());\n        String author = getOptimizedAuthor(feed, article.getAuthor());\n\n        return \"<!DOCTYPE html><html><head>\" +\n                \"<meta charset=\\\"UTF-8\\\">\" +\n                \"<link rel=\\\"stylesheet\\\" type=\\\"text/css\\\" href=\\\"./normalize.css\\\" />\" +\n                \"<title>\" + title + \"</title>\" +\n                \"</head><body>\" +\n                \"<article id=\\\"article\\\" >\" +\n                \"<header id=\\\"header\\\">\" +\n                \"<h1 id=\\\"title\\\"><a href=\\\"\" + link + \"\\\">\" + title + \"</a></h1>\" +\n                \"<p id=\\\"author\\\">\" + author + \"</p>\" +\n                \"<p id=\\\"pubDate\\\">\" + published + \"</p>\" +\n                \"</header>\" +\n                \"<section id=\\\"content\\\">\" + content + \"</section>\" +\n                \"</article>\" +\n                \"</body></html>\";\n    }\n\n\n//    public static String getPageForDisplay(Article article) {\n//        return getPageForDisplay(article, App.DISPLAY_RSS);\n//    }\n    public static String getPageForDisplay(Article article) { //, String referer\n\n        if (null == article) {\n            return \"\";\n        }\n        // 获取排版文件路径（支持自定义的文件）\n        String typesettingCssPath = App.i().getUserConfigPath() + \"normalize.css\";\n        if (!new File(typesettingCssPath).exists()) {\n            typesettingCssPath = \"file:///android_asset/css/normalize.css\";\n        }\n\n        // 获取排版文件路径（支持自定义的文件）\n        String mediaJsPath = App.i().getUserConfigPath() + \"media.js\";\n        if (!new File(mediaJsPath).exists()) {\n            mediaJsPath = \"file:///android_asset/js/media.js\";\n        }\n\n        // 获取主题文件路径\n        String themeCssPath, hljCSssPath;\n        if (App.i().getUser().getThemeMode() == App.THEME_DAY) {\n            themeCssPath = App.i().getUserConfigPath() + \"article_theme_day.css\";\n            if (!new File(typesettingCssPath).exists()) {\n                themeCssPath = \"file:///android_asset/css/article_theme_day.css\";\n            }\n        } else {\n            themeCssPath = App.i().getUserConfigPath() + \"article_theme_night.css\";\n            if (!new File(typesettingCssPath).exists()) {\n                themeCssPath = \"file:///android_asset/css/article_theme_night.css\";\n            }\n        }\n        hljCSssPath = \"file:///android_asset/css/android_studio.css\";\n\n        Feed feed = CoreDB.i().feedDao().getById(App.i().getUser().getId(), article.getFeedId());\n        String author = getOptimizedAuthor(feed, article.getAuthor());\n\n        String initImageHolderUrl =\n                \"var IMAGE_HOLDER_CLICK_TO_LOAD_URL = placeholder.getData({text: '\" + App.i().getString(R.string.click_to_load_this_picture) + \"'});\" +\n                \"var IMAGE_HOLDER_LOADING_URL = placeholder.getData({text: '\" + App.i().getString(R.string.loading) + \"'});\" +\n                \"var IMAGE_HOLDER_LOAD_FAILED_URL = placeholder.getData({text: '\" + App.i().getString(R.string.loading_failed_click_here_to_retry) + \"'});\" +\n                \"var IMAGE_HOLDER_IMAGE_ERROR_URL = placeholder.getData({text: '\" + App.i().getString(R.string.picture_error_click_here_to_retry) + \"'});\";\n        String content = getFormatContentForDisplay2(article);\n\n        String title = article.getTitle();\n        if (StringUtils.isEmpty(title)) {\n            title = App.i().getString(R.string.no_title);\n        }\n\n//        String readabilityButton = \"\";\n//        String displayMode;\n//        if (feed != null) {\n//            displayMode = TestConfig.i().getDisplayMode(feed.getId());\n//        } else {\n//            displayMode = App.DISPLAY_RSS;\n//        }\n\n        // 默认展示rss时，提示“获取全文”\n        // 默认展示Readability，提示“本文已自动/手动排版，如有问题点击查看原文、修复规则”\n//        if (StringUtils.isEmpty(displayMode) || !App.DISPLAY_LINK.equals(displayMode)) {\n//            if (!App.DISPLAY_READABILITY.equals(referer)) {\n//                readabilityButton = \"<br><br><a id='readability-button' onclick='\" + ArticleBridge.TAG + \".readability()'>获取全文</a>\";\n//            } else {\n//                readabilityButton = \"<br><br><a id='readability-button' onclick='\" + ArticleBridge.TAG + \".readability()'>恢复RSS内容</a>\";\n//            }\n//        }\n        return \"<!DOCTYPE html><html><head>\" +\n                \"<meta charset='UTF-8'>\" +\n                \"<meta name='referrer' content='origin'>\" +\n                \"<meta name='viewport' content='width=device-width, initial-scale=1.0, user-scalable=no'>\" +\n                \"<link rel='stylesheet' type='text/css' href='\" + typesettingCssPath + \"'/>\" +\n                \"<link rel='stylesheet' type='text/css' href='\" + themeCssPath + \"'/>\" +\n                \"<link rel='stylesheet' type='text/css' href='\" + hljCSssPath + \"'/>\" +\n                \"<title>\" + title + \"</title>\" +\n                \"</head><body>\" +\n                \"<article id='\" + article.getId() + \"'>\" +\n                \"<header id='header'>\" +\n                \"<h1 id='title'><a href='\" + article.getLink() + \"'>\" + title + \"</a></h1>\" +\n                \"<p id='author'>\" + author + \"</p>\" +\n                \"<p id='pubDate'>\" + TimeUtil.format(article.getPubDate(), \"yyyy-MM-dd HH:mm\") + \"</p>\" +\n                \"</header>\" +\n//                \"<hr id=\\\"hr\\\">\" +\n                \"<section id='content'>\" + content +\n//                readabilityButton +\n                \"</section>\" +\n                \"</article>\" +\n                \"<script src='file:///android_asset/js/zepto.min.js'></script>\" + // defer\n                \"<script src='file:///android_asset/js/lazyload.js'></script>\" +\n                \"<script src='file:///android_asset/js/highlight.pack.js'></script>\" +\n                \"<script src='file:///android_asset/js/placeholder.min.js'></script>\" +\n                \"<script>\" + initImageHolderUrl + \"</script>\" +\n                \"<script src='\" + mediaJsPath + \"'></script>\" +\n                \"<script>hljs.initHighlightingOnLoad();</script>\" +\n                \"</body></html>\";\n    }\n\n\n    public static String getContentForSpeak(Article article) {\n        String html = article.getContent();\n        Pattern pattern;\n        pattern = Pattern.compile(\"(<br>|<hr>|<p>|<pre>|<table>|<td>|<h\\\\d>|<ul>|<ol>|<li>)\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(\". $1\");\n\n        pattern = Pattern.compile(\"<img.*?>\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(App.i().getString(R.string.summary_image));\n        pattern = Pattern.compile(\"<(audio).*?>.*?</\\\\1>\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(App.i().getString(R.string.summary_audio));\n        pattern = Pattern.compile(\"<(video).*?>.*?</\\\\1>\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(App.i().getString(R.string.summary_video));\n\n        html = Jsoup.parse(html).text();\n\n        // 将网址替换为\n        pattern = Pattern.compile(\"https*://[\\\\w?-_=./&]*([\\\\s　]|&nbsp;|[^\\\\w?-_=./&]|$)\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(App.i().getString(R.string.summary_link) + \"$1\");\n\n        // KLog.i(\"初始化内容\" + html );\n        return App.i().getString(R.string.article_title_is) + article.getTitle() + html.trim();\n    }\n\n    /**\n     * 获取修整后的概要\n     *\n     * @param html 原文\n     * @return\n     */\n    public static String getOptimizedSummary(String html) {\n        if (StringUtils.isEmpty(html)) {\n            return html;\n        }\n        Pattern pattern;\n\n        pattern = Pattern.compile(\"(<br>|<hr>|<p>|<pre>|<table>|<td>|<h\\\\d>|<ul>|<ol>|<li>)\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(\"➤$1\");\n\n        pattern = Pattern.compile(\"<img.*?>\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(App.i().getString(R.string.image));\n        pattern = Pattern.compile(\"<(audio).*?>.*?</\\\\1>\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(App.i().getString(R.string.audio));\n        pattern = Pattern.compile(\"<(video).*?>.*?</\\\\1>\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(App.i().getString(R.string.video));\n        pattern = Pattern.compile(\"<(iframe).*?>.*?</\\\\1>\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(App.i().getString(R.string.frame));\n        pattern = Pattern.compile(\"<(embed).*?>.*?</\\\\1>\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(App.i().getString(R.string.frame));\n\n        html = Jsoup.parse(html).text();\n\n        // 将连续多个空格换为一个\n        pattern = Pattern.compile(\"([\\\\s　]|&nbsp;){2,}\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(\" \");\n\n        // 将连续多个➤合并为一个\n        pattern = Pattern.compile(\"(([\\\\s　]|&nbsp;)*➤([\\\\s　]|&nbsp;)*){2,}\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(\"➤\");\n\n        // 将某些符号前后的去掉\n        pattern = Pattern.compile(\"([\\\\s　]|&nbsp;)*➤+([\\\\s　]|&nbsp;)*([+_\\\\-=%@#$^&,，.。:：!！?？○●◎⊙☆★◇◆□■△▲〓\\\\[\\\\]“”()（）〔〕〈〉《》「」『』［］〖〗【】{}])\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(\"$3\");\n        pattern = Pattern.compile(\"([+_\\\\-=%@#$^&,，.。:：!！?？○●◎⊙☆★◇◆□■△▲〓\\\\[\\\\]“”()（）〔〕〈〉《》「」『』［］〖〗【】{}])([\\\\s　]|&nbsp;)*➤+([\\\\s　]|&nbsp;)*\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(\"$1\");\n\n        // 将开头的去掉\n        pattern = Pattern.compile(\"^\\\\s*➤*\\\\s*\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(\"\");\n\n        // 将末尾的去掉\n        pattern = Pattern.compile(\"\\\\s*➤*\\\\s*$\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(\"\");\n\n        // 给前后增加空格\n        pattern = Pattern.compile(\"([\\\\s　]|&nbsp;)*➤([\\\\s　]|&nbsp;)*\", Pattern.CASE_INSENSITIVE);\n        html = pattern.matcher(html).replaceAll(\" ➤ \");\n\n        html = html.substring(0,  Math.min(html.length(), 90) );\n        return html.trim();\n    }\n\n    /**\n     * 优化标题，去掉html转义、换行符\n     * @param title 文章标题\n     * @return\n     */\n    public static String getOptimizedTitle(String title) {\n        if (!StringUtils.isEmpty(title)) {\n            title = title.replace(\"\\r\", \"\").replace(\"\\n\", \"\");\n            title = Html.fromHtml(Html.fromHtml(title).toString()).toString();\n            return title;\n        }\n        return title;\n    }\n\n    /**\n     * 在将服务器的文章入库前，对文章进行修整，主要是过滤无用&有干扰的标签、属性\n     * @param articleUrl 文章链接\n     * @param content    原文\n     * @return\n     */\n    public static String getOptimizedContent(String articleUrl, String content) {\n        if (StringUtils.isEmpty(content)) {\n            return content;\n        }\n        Pattern pattern;\n        Matcher matcher;\n\n        // 过滤Ino广告\n        pattern = Pattern.compile(\"(?=<center>)[\\\\s\\\\S]*?inoreader[\\\\s\\\\S]*?(?<=</center>)\", Pattern.CASE_INSENSITIVE);\n        content = pattern.matcher(content).replaceAll(\"\");\n\n        // 删除无效的空标签（无任何属性的）\n        pattern = Pattern.compile(\"([\\\\s　]|&nbsp;)*<([a-zA-Z0-9]{1,10})>([\\\\s　]|&nbsp;)*</\\\\2>([\\\\s　]|&nbsp;)*\", Pattern.CASE_INSENSITIVE);\n        content = pattern.matcher(content).replaceAll(\"\");\n\n        // 删除无效的空标签（有属性的），注意此处的空标签必须是指定的，不然会把一些类似图片/音频/视频等“有意义的带属性空标签”给去掉\n        pattern = Pattern.compile(\"([\\\\s　]|&nbsp;)*<(i|p|section|div|figure|pre|table|blockquote) [^>/]+>([\\\\s　]|&nbsp;)*</\\\\2>([\\\\s　]|&nbsp;)*\", Pattern.CASE_INSENSITIVE);\n        content = pattern.matcher(content).replaceAll(\"\");\n\n        // 将包含<br>的空标签给解脱出来\n        pattern = Pattern.compile(\"([\\\\s　]|&nbsp;)*<([a-zA-Z0-9]{1,10})>([\\\\s　]|&nbsp;)*(<br>)+([\\\\s　]|&nbsp;)*</\\\\2>([\\\\s　]|&nbsp;)*\", Pattern.CASE_INSENSITIVE);\n        content = pattern.matcher(content).replaceAll(\"<br>\");\n\n        // 将包含<hr>的空标签给解脱出来\n        pattern = Pattern.compile(\"([\\\\s　]|&nbsp;)*<([a-zA-Z0-9]{1,10})>([\\\\s　]|&nbsp;)*(<hr>)+([\\\\s　]|&nbsp;)*</\\\\2>([\\\\s　]|&nbsp;)*\", Pattern.CASE_INSENSITIVE);\n        content = pattern.matcher(content).replaceAll(\"<hr>\");\n\n        // 去掉没有意义的span标签\n        pattern = Pattern.compile(\"[\\\\s　]*<span>([\\\\s\\\\S]*?)</span>[\\\\s　]*\", Pattern.CASE_INSENSITIVE);\n        content = pattern.matcher(content).replaceAll(\"$1\");\n\n        // 去掉块状元素之间的换行标签\n        pattern = Pattern.compile(\"</(<h\\\\d>|p|div|figure|pre|img|audio|video|iframe|embed|table|blockquote)>([\\\\s　]|&nbsp;)*(<br>)+([\\\\s　]|&nbsp;)*<(<h\\\\d>|p|div|figure|pre|img|audio|video|iframe|embed|table|blockquote)>\", Pattern.CASE_INSENSITIVE);\n        content = pattern.matcher(content).replaceAll(\"</$1><$5>\");\n\n        // 去掉文章开头以及，各种【标题头】后紧跟着的换行标签\n        pattern = Pattern.compile(\"(^|<h\\\\d>|<p>|<div>|<figure>|<pre>|<blockquote>)([\\\\s　]|&nbsp;)*(<br>|<hr>|<b>)+([\\\\s　]|&nbsp;)*\", Pattern.CASE_INSENSITIVE);\n        content = pattern.matcher(content).replaceAll(\"$1\");\n\n        // 去掉文章末尾的换行标签\n        pattern = Pattern.compile(\"([\\\\s　]|&nbsp;)*(<br>|<hr>|<b>)+([\\\\s　]|&nbsp;)*($|</h\\\\d>|</p>|</div>|</figure>|</pre>|</blockquote>)\", Pattern.CASE_INSENSITIVE);\n        content = pattern.matcher(content).replaceAll(\"$4\");\n\n        // 给两个连续的链接之间加一个换行符\n        pattern = Pattern.compile(\"</a>([\\\\s　]|&nbsp;)*<a\", Pattern.CASE_INSENSITIVE);\n        content = pattern.matcher(content).replaceAll(\"</a><br><a\");\n\n        Element element;\n        Elements elements;\n        Element documentBody = Jsoup.parseBodyFragment(content, articleUrl).body();\n        //KLog.e(\"内容C：\" + documentBody.html());\n\n        // 如果文章的开头就是 header 元素，则移除\n        elements = documentBody.children();\n        if( elements != null && elements.size() > 0 && elements.first().nodeName().equalsIgnoreCase(\"header\") ){\n            elements.first().remove();\n        }\n\n        // 去掉script标签\n        documentBody.getElementsByTag(\"script\").remove();\n        // 去掉style标签\n        documentBody.getElementsByTag(\"style\").remove();\n        // tabindex属性，会导致图片有边框\n        documentBody.removeAttr(\"tabindex\");\n        // video的controlslist属性可能会被配置为禁用下载/全屏，所以去掉\n        documentBody.removeAttr(\"controlslist\");\n        documentBody.removeAttr(\"dragable\");\n        documentBody.removeAttr(\"contenteditable\");\n        // img的crossorigin属性导致图片无法正确展示\n        documentBody.removeAttr(\"crossorigin\");\n        documentBody.removeAttr(\"class\");\n        \n        // 去掉src为空的标签\n        documentBody.select(\"[src=''],iframe:not([src]),embed:not([src])\").remove();\n\n        // 将 href 属性为空的 a 标签 unwrap\n        documentBody.select(\"[href=''],a:not([href])\").unwrap();\n\n        // 将 noscript 标签 unwrap\n        documentBody.getElementsByTag(\"noscript\").unwrap();\n        //KLog.e(\"内容V：\" + documentBody.html());\n        String tmp;\n        // \\s匹配的是 制表符\\t,换行符\\n,回车符\\r，换页符\\f以及半角空格\n        elements = documentBody.getElementsByTag(\"pre\");\n        for (int i = 0, size = elements.size(); i < size; i++) {\n            tmp = elements.get(i).html().trim();\n            pattern = Pattern.compile(\"([\\\\s　]|&nbsp;)*<([^>/]+)>([\\\\s　]|&nbsp;)*([\\\\s\\\\S]+)([\\\\s　]|&nbsp;)*</\\\\1>([\\\\s　]|&nbsp;)*\", Pattern.CASE_INSENSITIVE);\n            matcher = pattern.matcher(tmp);\n            if (matcher.matches()) {\n                tmp = pattern.matcher(tmp).replaceAll(\"<$2>$4</$2>\");\n                elements.get(i).html(tmp);\n            }\n        }\n        //KLog.e(\"内容D：\" + documentBody.html());\n        elements = documentBody.getElementsByTag(\"code\");\n        for (int i = 0, size = elements.size(); i < size; i++) {\n            tmp = elements.get(i).html().trim();\n            pattern = Pattern.compile(\"([\\\\s　]|&nbsp;)*<([^>/]+)>([\\\\s　]|&nbsp;)*([\\\\s\\\\S]+)([\\\\s　]|&nbsp;)*</\\\\1>([\\\\s　]|&nbsp;)*\", Pattern.CASE_INSENSITIVE);\n            matcher = pattern.matcher(tmp);\n            if (matcher.matches()) {\n                tmp = pattern.matcher(tmp).replaceAll(\"<$2>$4</$2>\");\n                elements.get(i).html(tmp);\n            }\n        }\n\n        // 将以下存放的原始src转为src的路径\n        String[] oriSrcAttr = {\"data-src\", \"data-original\", \"data-lazy-src\", \"zoomfile\", \"file\"};\n        for (String attr : oriSrcAttr) {\n            elements = documentBody.select(\"img[\" + attr + \"]\");\n            for (int i = 0, size = elements.size(); i < size; i++) {\n                element = elements.get(i);\n                tmp = element.attr(attr);\n                element.removeAttr(attr).attr(\"src\", tmp);\n            }\n        }\n\n        // picture 元素下会有一个标准的 img 元素，以及多个在不同条件下适配的 source 元素。\n        // 故先将 source 元素去掉，再将 picture unwrap，仅保留 img 元素\n        documentBody.select(\"picture > source\").remove();\n        documentBody.getElementsByTag(\"picture\").unwrap();\n\n        // 只保留 srcset 属性中尺寸最大的一张图片（该属性会根据屏幕分辨率选择想要显示的src）\n        elements = documentBody.select(\"img[srcset]\");\n        for (int i = 0, size = elements.size(); i < size; i++) {\n            element = elements.get(i);\n            String srcsetAttr = element.attr(\"srcset\");\n            if (StringUtils.isEmpty(srcsetAttr)) {\n                continue;\n            }\n            String[] srcset;\n            if (srcsetAttr.contains(\",\")) {\n                srcset = srcsetAttr.split(\",\");\n            } else {\n                srcset = new String[]{srcsetAttr};\n            }\n            int greaterDimen = 0;\n            String greaterSrc = null;\n            for (String srcDimen : srcset) {\n                pattern = Pattern.compile(\"(\\\\S+)\\\\s+(\\\\d*)[xXwW]\", Pattern.CASE_INSENSITIVE);\n                matcher = pattern.matcher(srcDimen);\n                if (!matcher.find()) {\n                    continue;\n                }\n                if (!StringUtils.isEmpty(matcher.group(2)) && Integer.parseInt(matcher.group(2)) > greaterDimen) {\n                    greaterSrc = matcher.group(1);\n                    greaterDimen = Integer.parseInt(matcher.group(2));\n                }\n            }\n            if (!StringUtils.isEmpty(greaterSrc)) {\n                elements.get(i).attr(\"src\", greaterSrc);\n            }\n            element.removeAttr(\"srcset\").removeAttr(\"sizes\");\n        }\n\n        // 去掉内联的css样式中的强制不换行\n        elements = documentBody.select(\"[style*=white-space]\");\n        for (int i = 0, size = elements.size(); i < size; i++) {\n            tmp = elements.get(i).attr(\"style\");\n            pattern = Pattern.compile(\"white-space.*?(;|$)\", Pattern.CASE_INSENSITIVE);\n            tmp = pattern.matcher(tmp).replaceAll(\"\");\n            elements.get(i).attr(\"style\", tmp);\n        }\n\n        // 将相对连接转为绝对链接\n        elements = documentBody.getElementsByAttribute(\"src\");\n        for (int i = 0, size = elements.size(); i < size; i++) {\n            element = elements.get(i);\n            element.attr(\"src\", element.attr(\"abs:src\"));\n        }\n        elements = documentBody.getElementsByAttribute(\"href\");\n        for (int i = 0, size = elements.size(); i < size; i++) {\n            element = elements.get(i);\n            tmp = element.attr(\"href\");\n            if(StringUtils.isEmpty(tmp) || tmp.startsWith(\"magnet:?\")){\n                continue;\n            }\n            element.attr(\"href\", element.attr(\"abs:href\"));\n        }\n\n        return documentBody.html().trim();\n    }\n\n    private static String getFormatContentForSave(String title, String content) {\n        Document document = Jsoup.parseBodyFragment(content);\n        Elements elements = document.getElementsByTag(\"img\");\n        String url, filePath;\n        for (int i = 0, size = elements.size(); i < size; i++) {\n            url = elements.get(i).attr(\"src\");\n            filePath = \"./\" + title + \"_files/\" + UriUtil.guessFileNameExt(url);\n            elements.get(i).attr(\"original-src\", url);\n            elements.get(i).attr(\"src\", filePath);\n        }\n        return document.body().html();\n    }\n\n\n    /**\n     * 格式化给定的文本，用于展示\n     * 这里没有直接将原始的文章内容给到 webView 加载，再去 webView 中初始化占位图并懒加载。\n     * 是因为这样 WebView 刚启动时，有的图片因为还没有被 js 替换为占位图，而展示一个错误图。\n     * 这里直接将内容初始化好，再让 WebView 执行懒加载的 js 去给没有加载本地图的 src 执行下载任务。\n     *\n     * @param article\n     * @return\n     */\n    private static String getFormatContentForDisplay2(Article article) {\n        if (StringUtils.isEmpty(article.getContent())) {\n            return \"\";\n        }\n        String originalUrl;\n        String imgHolder = \"file:///android_asset/image/image_holder.png\";\n\n        Element img;\n        Document document = Jsoup.parseBodyFragment(article.getContent(), article.getLink());\n        document = ColorModifier.i().inverseColor(document);\n\n        Elements elements;\n        String cacheUrl;\n        String idInMD5 = EncryptUtil.MD5(article.getId());\n\n        elements = document.getElementsByTag(\"img\");\n        for (int i = 0, size = elements.size(); i < size; i++) {\n            img = elements.get(i);\n            // 抽取图片的绝对连接\n            originalUrl = img.attr(\"abs:src\");\n            img.attr(\"original-src\", originalUrl);\n\n            cacheUrl = FileUtil.readCacheFilePath(idInMD5, originalUrl);\n            if (cacheUrl != null) {\n                img.attr(\"src\", cacheUrl);\n                img.addClass(\"image-holder\");\n            } else {\n                img.attr(\"src\", imgHolder);\n            }\n        }\n\n        elements = document.getElementsByTag(\"input\");\n        for (Element element : elements) {\n            element.attr(\"disabled\", \"disabled\");\n        }\n\n//        elements = document.getElementsByTag(\"video\");\n//        for (Element element : elements) {\n//            element.attr(\"controls\", \"true\")\n//                    .attr(\"width\", \"100%\")\n//                    .attr(\"height\", \"auto\")\n//                    .attr(\"preload\", \"metadata\");\n//        }\n\n//        elements = document.getElementsByTag(\"audio\");\n//        for (Element element : elements) {\n//            element.attr(\"controls\", \"true\")\n//                    .attr(\"width\", \"100%\");\n//        }\n\n//        // 给 table 包装 div 并配合 overflow-x: auto; ，让 table 内的 pre 不会撑出屏幕\n//        elements = document.getElementsByTag(\"table\");\n//        for (int i = 0, size = elements.size(); i < size; i++) {\n//            elements.get(i).wrap(\"<div class=\\\"table_wrap\\\"></div>\");\n//        }\n\n        return document.body().html().trim();\n    }\n    public static String getCoverUrl(String articleUrl, String content) {\n        // 获取第1个图片作为封面\n        Document document = Jsoup.parseBodyFragment(content,articleUrl);\n        Elements elements = document.getElementsByTag(\"img\");\n        String coverUrl = \"\";\n\n        if( elements != null && elements.size() > 0 ){\n            for (Element element:elements) {\n                coverUrl = element.attr(\"abs:src\");\n                if(!coverUrl.endsWith(\".svg\")){\n                    break;\n                }else {\n                    return coverUrl;\n                }\n            }\n        }\n\n\n        elements = document.select(\"video[poster]\");\n        if( elements != null && elements.size()>0 ){\n            coverUrl = elements.attr(\"abs:poster\");\n        }\n        return coverUrl;\n    }\n    \n    \n    private static String getOptimizedAuthor(Feed feed, String articleAuthor) {\n        if (null == feed) {\n            if (StringUtils.isEmpty(articleAuthor)) {\n                return \"\";\n            }else {\n                return articleAuthor;\n            }\n        }\n\n        String articleAuthorLowerCase = StringUtils.isEmpty(articleAuthor) ? \"\" : articleAuthor.toLowerCase();\n        String feedTitleLowerCase = StringUtils.isEmpty(feed.getTitle()) ? \"\" : feed.getTitle().toLowerCase();\n\n        if (feedTitleLowerCase.contains(articleAuthorLowerCase)) {\n            return feed.getTitle();\n        } else if (articleAuthorLowerCase.contains(feedTitleLowerCase)) {\n            return articleAuthor;\n        } else {\n            return feed.getTitle() + \" / \" + articleAuthor;\n        }\n    }\n\n\n    public static String getOptimizedContentWithEnclosures(String content, List<Enclosure> attachments){\n        // 获取视频或者音频附件\n        if (attachments != null && attachments.size() != 0) {\n            for (Enclosure enclosure : attachments) {\n                if (StringUtils.isEmpty(enclosure.getType()) || StringUtils.isEmpty(enclosure.getHref()) || content.contains(enclosure.getHref())) {\n                    continue;\n                }\n                if (!StringUtils.isEmpty(content)){\n                    content = content + \"<br>\";\n                }\n                if (enclosure.getType().startsWith(\"image\")) {\n                    content = content + \"<img src=\\\"\" + enclosure.getHref() + \"\\\"/>\";\n                } else if (enclosure.getType().startsWith(\"audio\")) {\n                    content = content + \"<audio src=\\\"\" + enclosure.getHref() + \"\\\" preload=\\\"auto\\\" type=\\\"\" + enclosure.getType() + \"\\\" controls></audio>\";\n                } else if (enclosure.getType().startsWith(\"video\")) {\n                    content = content + \"<video src=\\\"\" + enclosure.getHref() + \"\\\" preload=\\\"auto\\\" type=\\\"\" + enclosure.getType() + \"\\\" controls></video>\";\n                } else if(enclosure.getType().equalsIgnoreCase(\"application/x-shockwave-flash\")){\n                    content = content + \"<iframe src=\\\"\" + enclosure.getHref() + \"\\\"></iframe>\";\n                }\n            }\n        }\n        return content;\n    }\n\n    public static Article getReadabilityArticle(Article article, ResponseBody responseBody) throws IOException{\n        MediaType mediaType  = responseBody.contentType();\n        String charset = null;\n        if( mediaType != null ){\n            charset = DataUtil.getCharsetFromContentType(mediaType.toString());\n        }\n        KLog.i(\"解析得到的编码为：\" + mediaType + \" ， \"+  charset );\n\n        // 换parser吧，jsoup默认使用是htmlParser，它会对返回内容做些改动来符合html规范，所以一般实际使用时都用的是xmlParser，代码如下\n        Document doc;\n        try {\n            doc = Jsoup.parse(responseBody.byteStream(), charset, article.getLink());\n        }catch (IOException e){\n            throw e;\n        }\n        doc.outputSettings().prettyPrint(false);\n        String content =  ExtractorUtil.getContent(article.getLink(), doc);\n        content = ArticleUtil.getOptimizedContent(article.getLink(), content);\n        //KLog.e(\"内容B：\" + content);\n        Article newArticle = (Article)article.clone();\n        newArticle.setContent(content);\n\n        String summary = ArticleUtil.getOptimizedSummary(content);\n        newArticle.setSummary(summary);\n\n        String coverUrl = ArticleUtil.getCoverUrl(article.getLink(), content);\n\n        if(!StringUtils.isEmpty(coverUrl)){\n            newArticle.setImage(coverUrl);\n        }else if( !StringUtils.isEmpty(article.getImage()) ){\n            newArticle.setImage(null);\n        }\n        return newArticle;\n    }\n\n\n\n\n//    public static void autoSetArticleTags(Article article){\n//        List<Category> categories = CoreDB.i().categoryDao().getByFeedId(article.getUid(),article.getFeedId());\n//        List<ArticleTag> articleTags = CoreDB.i().articleTagDao().getByArticleId(article.getUid(),article.getId());\n//        if(categories != null && categories.size() > 0 && (articleTags == null || articleTags.size() == 0)){\n//            articleTags = new ArrayList<>(categories.size());\n//            for (Category category:categories) {\n//                Tag tag = new Tag();\n//                tag.setUid(article.getUid());\n//                tag.setId(category.getTitle());\n//                tag.setTitle(category.getTitle());\n//                ArticleTag articleTag = new ArticleTag(article.getUid(),article.getId(),tag.getId());\n//                articleTags.add(articleTag);\n//            }\n//            CoreDB.i().articleTagDao().insert(articleTags);\n//            ArticleTags.i().addArticleTags(articleTags);\n//            ArticleTags.i().save();\n//        }\n//    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/ColorModifier.java",
    "content": "package me.wizos.loread.utils;\n\nimport org.jsoup.nodes.Document;\nimport org.jsoup.nodes.Element;\nimport org.jsoup.select.Elements;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport me.wizos.loread.App;\n\n/**\n * Created by Wizos on 2019/6/7.\n */\n\npublic class ColorModifier {\n    private static ColorModifier app;\n\n    private ColorModifier() {\n    }\n\n    public static ColorModifier i() {\n        if (app == null) {\n            synchronized (ColorModifier.class) {\n                if (app == null) {\n                    app = new ColorModifier();\n                }\n            }\n        }\n        return app;\n    }\n\n    private Matcher m;\n    private String tmp = \"\";\n    private int r = 0, g = 0, b = 0;\n\n    public Document handleColor(Document doc) {\n        RGB backgroundColor;\n        if (App.i().getUser().getThemeMode() == App.THEME_DAY) {\n            backgroundColor = new RGB(255, 255, 255);\n        } else {\n            // return new RGB(69, 73, 82);\n            backgroundColor = new RGB(32, 43, 47);\n        }\n\n        Elements elements;\n        doc.select(\"[bgcolor]\").removeAttr(\"bgcolor\");\n        elements = doc.select(\"[style*=color]\");\n        for (Element element : elements) {\n            tmp = element.attr(\"style\");\n            // 去掉原生背景色\n            m = Pattern.compile(\"background(\\\\s*:|-color).*?(;|$)\", Pattern.CASE_INSENSITIVE).matcher(tmp);\n            tmp = m.replaceAll(\"\");\n            element.attr(\"style\", tmp);\n\n            // 优化前景色\n            m = Pattern.compile(\"(^|\\\\s*)color\\\\s*:(.*?)($|;)\", Pattern.CASE_INSENSITIVE).matcher(tmp);\n            if (m.find()) {\n                tmp = m.replaceFirst(\"color:\" + modifyColor(m.group(2), backgroundColor) + \";\");\n                element.attr(\"style\", tmp);\n            }\n        }\n\n        elements = doc.select(\"[color]\");\n        for (Element element : elements) {\n            element.attr(\"style\", \"color:\" + modifyColor(element.attr(\"color\"), backgroundColor));\n            element.removeAttr(\"color\");\n        }\n        return doc;\n    }\n\n    public Document inverseColor(Document doc) {\n        if (App.i().getUser().getThemeMode() == App.THEME_DAY) {\n            return inverseColor(doc, new RGB(255, 255, 255));\n        } else {\n            // return new RGB(69, 73, 82);\n            return inverseColor(doc, new RGB(32, 43, 47));\n        }\n    }\n\n    private Document inverseColor(Document doc, RGB backgroundColor) {\n        Elements elements;\n        doc.select(\"[bgcolor]\").removeAttr(\"bgcolor\");\n        elements = doc.select(\"[style*=color]\");\n        for (Element element : elements) {\n            tmp = element.attr(\"style\");\n            // 先去掉背景色\n            m = Pattern.compile(\"background(\\\\s*:|-color).*?(;|$)\", Pattern.CASE_INSENSITIVE).matcher(tmp);\n            tmp = m.replaceAll(\"\");\n            element.attr(\"style\", tmp);\n\n            m = Pattern.compile(\"(^|\\\\s*)color\\\\s*:(.*?)($|;)\", Pattern.CASE_INSENSITIVE).matcher(tmp);\n            if (m.find()) {\n                tmp = m.replaceFirst(\"color:\" + modifyColor(m.group(2), backgroundColor) + \";\");\n                element.attr(\"style\", tmp);\n            }\n        }\n\n        elements = doc.select(\"[color]\");\n        for (Element element : elements) {\n            //element.attr(\"color\",modifyColor(element.attr(\"color\"),backgroundColor) );\n            element.attr(\"style\", \"color:\" + modifyColor(element.attr(\"color\"), backgroundColor));\n            element.removeAttr(\"color\");\n        }\n        return doc;\n    }\n\n    private String modifyColor(String color, RGB backgroundColor) {\n        // 处理 RGB 颜色\n        m = Pattern.compile(\"\\\\(\\\\s*(\\\\d+)\\\\s*,\\\\s*(\\\\d+)\\\\s*,\\\\s*(\\\\d+)\\\\s*\\\\)\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            r = Integer.valueOf(m.group(1));\n            g = Integer.valueOf(m.group(2));\n            b = Integer.valueOf(m.group(3));\n            return modifyColor(backgroundColor, new RGB(r, g, b));\n        }\n\n        // 处理 #FF000000 类型的颜色\n        m = Pattern.compile(\"#\\\\W*\\\\w{2}(\\\\w{6})\\\\W*$\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            r = Integer.parseInt(m.group(1).substring(0, 2), 16);\n            g = Integer.parseInt(m.group(1).substring(2, 4), 16);\n            b = Integer.parseInt(m.group(1).substring(4, 6), 16);\n            return modifyColor(backgroundColor, new RGB(r, g, b));\n        }\n\n        // 处理 #000000 类型的颜色\n        m = Pattern.compile(\"#\\\\W*(\\\\w{6})\\\\W*$\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            r = Integer.parseInt(m.group(1).substring(0, 2), 16);\n            g = Integer.parseInt(m.group(1).substring(2, 4), 16);\n            b = Integer.parseInt(m.group(1).substring(4, 6), 16);\n            return modifyColor(backgroundColor, new RGB(r, g, b));\n        }\n\n        // 处理 #0F0 类型的颜色\n        m = Pattern.compile(\"#\\\\W*(\\\\w{3})\\\\W*$\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            tmp = m.group(1).substring(0, 1);\n            r = Integer.parseInt(tmp + tmp, 16);\n            tmp = m.group(1).substring(1, 2);\n            g = Integer.parseInt(tmp + tmp, 16);\n            tmp = m.group(1).substring(2, 3);\n            b = Integer.parseInt(tmp + tmp, 16);\n            return modifyColor(backgroundColor, new RGB(r, g, b));\n        }\n\n        // 处理颜色\n        m = Pattern.compile(\"LightPink\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 182, 193));\n        }\n        m = Pattern.compile(\"Pink\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 192, 203));\n        }\n        m = Pattern.compile(\"Crimson\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(220, 20, 60));\n        }\n        m = Pattern.compile(\"LavenderBlush\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 240, 245));\n        }\n        m = Pattern.compile(\"PaleVioletRed\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(219, 112, 147));\n        }\n        m = Pattern.compile(\"HotPink\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 105, 180));\n        }\n        m = Pattern.compile(\"DeepPink\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 20, 147));\n        }\n        m = Pattern.compile(\"MediumVioletRed\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(199, 21, 133));\n        }\n        m = Pattern.compile(\"Orchid\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(218, 112, 214));\n        }\n        m = Pattern.compile(\"Thistle\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(216, 191, 216));\n        }\n        m = Pattern.compile(\"plum\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(221, 160, 221));\n        }\n        m = Pattern.compile(\"Violet\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(238, 130, 238));\n        }\n        m = Pattern.compile(\"Magenta\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 0, 255));\n        }\n        m = Pattern.compile(\"Fuchsia\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 0, 255));\n        }\n        m = Pattern.compile(\"DarkMagenta\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(139, 0, 139));\n        }\n        m = Pattern.compile(\"Purple\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(128, 0, 128));\n        }\n        m = Pattern.compile(\"MediumOrchid\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(186, 85, 211));\n        }\n        m = Pattern.compile(\"DarkVoilet\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(148, 0, 211));\n        }\n        m = Pattern.compile(\"DarkOrchid\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(153, 50, 204));\n        }\n        m = Pattern.compile(\"Indigo\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(75, 0, 130));\n        }\n        m = Pattern.compile(\"BlueViolet\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(138, 43, 226));\n        }\n        m = Pattern.compile(\"MediumPurple\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(147, 112, 219));\n        }\n        m = Pattern.compile(\"MediumSlateBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(123, 104, 238));\n        }\n        m = Pattern.compile(\"SlateBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(106, 90, 205));\n        }\n        m = Pattern.compile(\"DarkSlateBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(72, 61, 139));\n        }\n        m = Pattern.compile(\"Lavender\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(230, 230, 250));\n        }\n        m = Pattern.compile(\"GhostWhite\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(248, 248, 255));\n        }\n        m = Pattern.compile(\"Blue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 0, 255));\n        }\n        m = Pattern.compile(\"MediumBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 0, 205));\n        }\n        m = Pattern.compile(\"MidnightBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(25, 25, 112));\n        }\n        m = Pattern.compile(\"DarkBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 0, 139));\n        }\n        m = Pattern.compile(\"Navy\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 0, 128));\n        }\n        m = Pattern.compile(\"RoyalBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(65, 105, 225));\n        }\n        m = Pattern.compile(\"CornflowerBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(100, 149, 237));\n        }\n        m = Pattern.compile(\"LightSteelBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(176, 196, 222));\n        }\n        m = Pattern.compile(\"LightSlateGray\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(119, 136, 153));\n        }\n        m = Pattern.compile(\"SlateGray\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(112, 128, 144));\n        }\n        m = Pattern.compile(\"DodgerBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(30, 144, 255));\n        }\n        m = Pattern.compile(\"AliceBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(240, 248, 255));\n        }\n        m = Pattern.compile(\"SteelBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(70, 130, 180));\n        }\n        m = Pattern.compile(\"LightSkyBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(135, 206, 250));\n        }\n        m = Pattern.compile(\"SkyBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(135, 206, 235));\n        }\n        m = Pattern.compile(\"DeepSkyBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 191, 255));\n        }\n        m = Pattern.compile(\"LightBLue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(173, 216, 230));\n        }\n        m = Pattern.compile(\"PowDerBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(176, 224, 230));\n        }\n        m = Pattern.compile(\"CadetBlue\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(95, 158, 160));\n        }\n        m = Pattern.compile(\"Azure\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(240, 255, 255));\n        }\n        m = Pattern.compile(\"LightCyan\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(225, 255, 255));\n        }\n        m = Pattern.compile(\"PaleTurquoise\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(175, 238, 238));\n        }\n        m = Pattern.compile(\"Cyan\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 255, 255));\n        }\n        m = Pattern.compile(\"Aqua\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 255, 255));\n        }\n        m = Pattern.compile(\"DarkTurquoise\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 206, 209));\n        }\n        m = Pattern.compile(\"DarkSlateGray\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(47, 79, 79));\n        }\n        m = Pattern.compile(\"DarkCyan\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 139, 139));\n        }\n        m = Pattern.compile(\"Teal\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 128, 128));\n        }\n        m = Pattern.compile(\"MediumTurquoise\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(72, 209, 204));\n        }\n        m = Pattern.compile(\"LightSeaGreen\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(32, 178, 170));\n        }\n        m = Pattern.compile(\"Turquoise\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(64, 224, 208));\n        }\n        m = Pattern.compile(\"BabyGreen\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(127, 255, 170));\n        }\n        m = Pattern.compile(\"MediumAquamarine\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 250, 154));\n        }\n        m = Pattern.compile(\"MediumSpringGreen\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(245, 255, 250));\n        }\n        m = Pattern.compile(\"MintCream\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 255, 127));\n        }\n        m = Pattern.compile(\"SpringGreen\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(60, 179, 113));\n        }\n        m = Pattern.compile(\"SeaGreen\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(46, 139, 87));\n        }\n        m = Pattern.compile(\"Honeydew\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(240, 255, 0));\n        }\n        m = Pattern.compile(\"LightGreen\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(144, 238, 144));\n        }\n        m = Pattern.compile(\"PaleGreen\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(152, 251, 152));\n        }\n        m = Pattern.compile(\"DarkSeaGreen\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(143, 188, 143));\n        }\n        m = Pattern.compile(\"LimeGreen\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(50, 205, 50));\n        }\n        m = Pattern.compile(\"Lime\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 255, 0));\n        }\n        m = Pattern.compile(\"ForestGreen\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(34, 139, 34));\n        }\n        m = Pattern.compile(\"Green\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 128, 0));\n        }\n        m = Pattern.compile(\"DarkGreen\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 100, 0));\n        }\n        m = Pattern.compile(\"Chartreuse\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(127, 255, 0));\n        }\n        m = Pattern.compile(\"LawnGreen\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(124, 252, 0));\n        }\n        m = Pattern.compile(\"GreenYellow\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(173, 255, 47));\n        }\n        m = Pattern.compile(\"OliveDrab\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(85, 107, 47));\n        }\n        m = Pattern.compile(\"Beige\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(107, 142, 35));\n        }\n        m = Pattern.compile(\"LightGoldenrodYellow\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(250, 250, 210));\n        }\n        m = Pattern.compile(\"Ivory\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 255, 240));\n        }\n        m = Pattern.compile(\"LightYellow\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 255, 224));\n        }\n        m = Pattern.compile(\"Yellow\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 255, 0));\n        }\n        m = Pattern.compile(\"Olive\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(128, 128, 0));\n        }\n        m = Pattern.compile(\"DarkKhaki\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(189, 183, 107));\n        }\n        m = Pattern.compile(\"LemonChiffon\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 250, 205));\n        }\n        m = Pattern.compile(\"PaleGodenrod\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(238, 232, 170));\n        }\n        m = Pattern.compile(\"Khaki\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(240, 230, 140));\n        }\n        m = Pattern.compile(\"Gold\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 215, 0));\n        }\n        m = Pattern.compile(\"Cornislk\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 248, 220));\n        }\n        m = Pattern.compile(\"GoldEnrod\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(218, 165, 32));\n        }\n        m = Pattern.compile(\"FloralWhite\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 250, 240));\n        }\n        m = Pattern.compile(\"OldLace\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(253, 245, 230));\n        }\n        m = Pattern.compile(\"Wheat\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(245, 222, 179));\n        }\n        m = Pattern.compile(\"Moccasin\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 228, 181));\n        }\n        m = Pattern.compile(\"Orange\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 165, 0));\n        }\n        m = Pattern.compile(\"PapayaWhip\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 239, 213));\n        }\n        m = Pattern.compile(\"BlanchedAlmond\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 235, 205));\n        }\n        m = Pattern.compile(\"NavajoWhite\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 222, 173));\n        }\n        m = Pattern.compile(\"AntiqueWhite\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(250, 235, 215));\n        }\n        m = Pattern.compile(\"Tan\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(210, 180, 140));\n        }\n        m = Pattern.compile(\"BrulyWood\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(222, 184, 135));\n        }\n        m = Pattern.compile(\"Bisque\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 228, 196));\n        }\n        m = Pattern.compile(\"DarkOrange\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 140, 0));\n        }\n        m = Pattern.compile(\"Linen\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(250, 240, 230));\n        }\n        m = Pattern.compile(\"Peru\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(205, 133, 63));\n        }\n        m = Pattern.compile(\"PeachPuff\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 218, 185));\n        }\n        m = Pattern.compile(\"SandyBrown\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(244, 164, 96));\n        }\n        m = Pattern.compile(\"Chocolate\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(210, 105, 30));\n        }\n        m = Pattern.compile(\"SaddleBrown\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(139, 69, 19));\n        }\n        m = Pattern.compile(\"SeaShell\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 245, 238));\n        }\n        m = Pattern.compile(\"Sienna\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(160, 82, 45));\n        }\n        m = Pattern.compile(\"LightSalmon\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 160, 122));\n        }\n        m = Pattern.compile(\"Coral\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 127, 80));\n        }\n        m = Pattern.compile(\"OrangeRed\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 69, 0));\n        }\n        m = Pattern.compile(\"DarkSalmon\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(233, 150, 122));\n        }\n        m = Pattern.compile(\"Tomato\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 99, 71));\n        }\n        m = Pattern.compile(\"MistyRose\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 228, 225));\n        }\n        m = Pattern.compile(\"Salmon\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(250, 128, 114));\n        }\n        m = Pattern.compile(\"Snow\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 250, 250));\n        }\n        m = Pattern.compile(\"LightCoral\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(240, 128, 128));\n        }\n        m = Pattern.compile(\"RosyBrown\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(188, 143, 143));\n        }\n        m = Pattern.compile(\"IndianRed\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(205, 92, 92));\n        }\n        m = Pattern.compile(\"Red\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 0, 0));\n        }\n        m = Pattern.compile(\"Brown\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(165, 42, 42));\n        }\n        m = Pattern.compile(\"FireBrick\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(178, 34, 34));\n        }\n        m = Pattern.compile(\"DarkRed\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(139, 0, 0));\n        }\n        m = Pattern.compile(\"Maroon\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(128, 0, 0));\n        }\n        m = Pattern.compile(\"White\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(255, 255, 255));\n        }\n        m = Pattern.compile(\"WhiteSmoke\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(245, 245, 245));\n        }\n        m = Pattern.compile(\"Gainsboro\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(220, 220, 220));\n        }\n        m = Pattern.compile(\"LightGray\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(211, 211, 211));\n        }\n        m = Pattern.compile(\"Silver\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(192, 192, 192));\n        }\n        m = Pattern.compile(\"DarkGray\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(169, 169, 169));\n        }\n        m = Pattern.compile(\"Gray\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(128, 128, 128));\n        }\n        m = Pattern.compile(\"DimGray\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(105, 105, 105));\n        }\n        m = Pattern.compile(\"Black\", Pattern.CASE_INSENSITIVE).matcher(color);\n        if (m.find()) {\n            return modifyColor(backgroundColor, new RGB(0, 0, 0));\n        }\n        return color;\n    }\n\n    /**\n     * 计算2个颜色的距离，若小于阈值则返回反色\n     */\n    private String modifyColor(RGB e1, RGB e2) {\n        // System.out.println(\"对比颜色：\" + e1 + \"   =   \" +e2 );\n        long rmean = ((long) e1.r + (long) e2.r) / 2;\n        long r = (long) e1.r - (long) e2.r;\n        long g = (long) e1.g - (long) e2.g;\n        long b = (long) e1.b - (long) e2.b;\n        // 黑白色距离约764.8，这里居中取382\n        if (Math.sqrt((((512 + rmean) * r * r) >> 8) + 4 * g * g + (((767 - rmean) * b * b) >> 8)) < 382) {\n            return \"rgb(\" + getInverseColor(e2.r) + \", \" + getInverseColor(e2.g) + \", \" + getInverseColor(e2.b) + \")\";\n//            return \"#\" + Integer.toHexString(getInverseColor(e2.r)) + Integer.toHexString(getInverseColor(e2.g)) + Integer.toHexString(getInverseColor(e2.b));\n        } else {\n            return \"rgb(\" + e2.r + \", \" + e2.g + \", \" + e2.b + \")\";\n        }\n    }\n\n\n    // https://blog.csdn.net/do168/article/details/51619656\n    private static int getInverseColor(int color) {\n        return 255 - color;\n    }\n\n    private static int getInverseColor2(int color) {\n        if (color > 64 && color < 128)\n            color -= 64;\n        else if (color >= 128 && color < 192)\n            color += 64;\n        return 255 - color;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/DataUtil.java",
    "content": "package me.wizos.loread.utils;\n\nimport org.jsoup.UncheckedIOException;\nimport org.jsoup.helper.Validate;\nimport org.jsoup.internal.ConstrainableInputStream;\nimport org.jsoup.nodes.Document;\nimport org.jsoup.nodes.Element;\nimport org.jsoup.nodes.XmlDeclaration;\nimport org.jsoup.parser.Parser;\nimport org.jsoup.select.Elements;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.OutputStream;\nimport java.io.RandomAccessFile;\nimport java.nio.Buffer;\nimport java.nio.ByteBuffer;\nimport java.nio.charset.Charset;\nimport java.nio.charset.IllegalCharsetNameException;\nimport java.util.Locale;\nimport java.util.Random;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Created by Wizos on 2018/7/7.\n */\n\npublic class DataUtil {\n    private static final Pattern charsetPattern = Pattern.compile(\"(?i)\\\\bcharset=\\\\s*(?:[\\\"'])?([^\\\\s,;\\\"']*)\");\n    static final String defaultCharset = \"UTF-8\"; // used if not found in header or meta charset\n    private static final int firstReadBufferSize = 1024 * 5;\n    static final int bufferSize = 1024 * 32;\n    private static final char[] mimeBoundaryChars =\n            \"-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\".toCharArray();\n    static final int boundaryLength = 32;\n\n    private DataUtil() {\n    }\n\n    /**\n     * Loads a file to a Document.\n     *\n     * @param in          file to load\n     * @param charsetName character set of input\n     * @param baseUri     base URI of document, to resolve relative links against\n     * @return Document\n     * @throws IOException on IO error\n     */\n    public static Document load(File in, String charsetName, String baseUri) throws IOException {\n        return parseInputStream(new FileInputStream(in), charsetName, baseUri, Parser.htmlParser());\n    }\n\n    /**\n     * Parses a Document from an input steam.\n     *\n     * @param in          input stream to parse. You will need to close it.\n     * @param charsetName character set of input\n     * @param baseUri     base URI of document, to resolve relative links against\n     * @return Document\n     * @throws IOException on IO error\n     */\n    public static Document load(InputStream in, String charsetName, String baseUri) throws IOException {\n        return parseInputStream(in, charsetName, baseUri, Parser.htmlParser());\n    }\n\n    /**\n     * Parses a Document from an input steam, using the provided Parser.\n     *\n     * @param in          input stream to parse. You will need to close it.\n     * @param charsetName character set of input\n     * @param baseUri     base URI of document, to resolve relative links against\n     * @param parser      alternate {@link Parser#xmlParser() parser} to use.\n     * @return Document\n     * @throws IOException on IO error\n     */\n    public static Document load(InputStream in, String charsetName, String baseUri, Parser parser) throws IOException {\n        return parseInputStream(in, charsetName, baseUri, parser);\n    }\n\n    /**\n     * Writes the input stream to the output stream. Doesn't close them.\n     *\n     * @param in  input stream to read from\n     * @param out output stream to write to\n     * @throws IOException on IO error\n     */\n    static void crossStreams(final InputStream in, final OutputStream out) throws IOException {\n        final byte[] buffer = new byte[bufferSize];\n        int len;\n        while ((len = in.read(buffer)) != -1) {\n            out.write(buffer, 0, len);\n        }\n    }\n\n    public static Document parseInputStream(InputStream input, String charsetName, String baseUri, Parser parser) throws IOException {\n        if (input == null) // empty body\n            return new Document(baseUri);\n        input = ConstrainableInputStream.wrap(input, bufferSize, 0);\n\n        Document doc = null;\n        boolean fullyRead = false;\n\n        // read the start of the stream and look for a BOM or meta charset\n        input.mark(bufferSize);\n        ByteBuffer firstBytes = readToByteBuffer(input, firstReadBufferSize - 1); // -1 because we read one more to see if completed. First read is < buffer size, so can't be invalid.\n        fullyRead = input.read() == -1;\n        input.reset();\n\n        // look for BOM - overrides any other header or input\n        BomCharset bomCharset = detectCharsetFromBom(firstBytes);\n        if (bomCharset != null)\n            charsetName = bomCharset.charset;\n\n        if (charsetName == null) { // determine from meta. safe first parse as UTF-8\n            String docData = Charset.forName(defaultCharset).decode(firstBytes).toString();\n            doc = parser.parseInput(docData, baseUri);\n\n            // look for <meta http-equiv=\"Content-Type\" content=\"text/html;charset=gb2312\"> or HTML5 <meta charset=\"gb2312\">\n            Elements metaElements = doc.select(\"meta[http-equiv=content-type], meta[charset]\");\n            String foundCharset = null; // if not found, will keep utf-8 as best attempt\n            for (Element meta : metaElements) {\n                if (meta.hasAttr(\"http-equiv\"))\n                    foundCharset = getCharsetFromContentType(meta.attr(\"content\"));\n                if (foundCharset == null && meta.hasAttr(\"charset\"))\n                    foundCharset = meta.attr(\"charset\");\n                if (foundCharset != null)\n                    break;\n            }\n\n            // look for <?xml encoding='ISO-8859-1'?>\n            if (foundCharset == null && doc.childNodeSize() > 0 && doc.childNode(0) instanceof XmlDeclaration) {\n                XmlDeclaration prolog = (XmlDeclaration) doc.childNode(0);\n                if (prolog.name().equals(\"xml\"))\n                    foundCharset = prolog.attr(\"encoding\");\n            }\n            foundCharset = validateCharset(foundCharset);\n            if (foundCharset != null && !foundCharset.equalsIgnoreCase(defaultCharset)) { // need to re-decode. (case insensitive check here to match how validate works)\n                foundCharset = foundCharset.trim().replaceAll(\"[\\\"']\", \"\");\n                charsetName = foundCharset;\n                doc = null;\n            } else if (!fullyRead) {\n                doc = null;\n            }\n        } else { // specified by content type header (or by user on file load)\n            Validate.notEmpty(charsetName, \"Must set charset arg to character set of file to parse. Set to null to attempt to detect from HTML\");\n        }\n        if (doc == null) {\n            if (charsetName == null)\n                charsetName = defaultCharset;\n            BufferedReader reader = new BufferedReader(new InputStreamReader(input, charsetName), bufferSize);\n            if (bomCharset != null && bomCharset.offset) // creating the buffered reader ignores the input pos, so must skip here\n                reader.skip(1);\n            try {\n                doc = parser.parseInput(reader, baseUri);\n            } catch (UncheckedIOException e) {\n                // io exception when parsing (not seen before because reading the stream as we go)\n                throw e.ioException();\n            }\n            doc.outputSettings().charset(charsetName);\n        }\n        input.close();\n        return doc;\n    }\n\n    /**\n     * Read the input stream into a byte buffer. To deal with slow input streams, you may interrupt the thread this\n     * method is executing on. The data read until being interrupted will be available.\n     *\n     * @param inStream the input stream to read from\n     * @param maxSize  the maximum size in bytes to read from the stream. Set to 0 to be unlimited.\n     * @return the filled byte buffer\n     * @throws IOException if an exception occurs whilst reading from the input stream.\n     */\n    public static ByteBuffer readToByteBuffer(InputStream inStream, int maxSize) throws IOException {\n        Validate.isTrue(maxSize >= 0, \"maxSize must be 0 (unlimited) or larger\");\n        final ConstrainableInputStream input = ConstrainableInputStream.wrap(inStream, bufferSize, maxSize);\n        return input.readToByteBuffer(maxSize);\n    }\n\n    static ByteBuffer readToByteBuffer(InputStream inStream) throws IOException {\n        return readToByteBuffer(inStream, 0);\n    }\n\n    static ByteBuffer readFileToByteBuffer(File file) throws IOException {\n        RandomAccessFile randomAccessFile = null;\n        try {\n            randomAccessFile = new RandomAccessFile(file, \"r\");\n            byte[] bytes = new byte[(int) randomAccessFile.length()];\n            randomAccessFile.readFully(bytes);\n            return ByteBuffer.wrap(bytes);\n        } finally {\n            if (randomAccessFile != null)\n                randomAccessFile.close();\n        }\n    }\n\n    static ByteBuffer emptyByteBuffer() {\n        return ByteBuffer.allocate(0);\n    }\n\n    /**\n     * Parse out a charset from a content type header. If the charset is not supported, returns null (so the default\n     * will kick in.)\n     *\n     * @param contentType e.g. \"text/html; charset=EUC-JP\"\n     * @return \"EUC-JP\", or null if not found. Charset is trimmed and uppercased.\n     */\n    public static String getCharsetFromContentType(String contentType) {\n        if (contentType == null) return null;\n        Matcher m = charsetPattern.matcher(contentType);\n        if (m.find()) {\n            String charset = m.group(1).trim();\n            charset = charset.replace(\"charset=\", \"\");\n            return validateCharset(charset);\n        }\n        return null;\n    }\n\n    private static String validateCharset(String cs) {\n        if (cs == null || cs.length() == 0) return null;\n        cs = cs.trim().replaceAll(\"[\\\"']\", \"\");\n        try {\n            if (Charset.isSupported(cs)) return cs;\n            cs = cs.toUpperCase(Locale.ENGLISH);\n            if (Charset.isSupported(cs)) return cs;\n        } catch (IllegalCharsetNameException e) {\n            // if our this charset matching fails.... we just take the default\n        }\n        return null;\n    }\n\n    /**\n     * Creates a random string, suitable for use as a mime boundary\n     */\n    static String mimeBoundary() {\n        final StringBuilder mime = new StringBuilder(boundaryLength);\n        final Random rand = new Random();\n        for (int i = 0; i < boundaryLength; i++) {\n            mime.append(mimeBoundaryChars[rand.nextInt(mimeBoundaryChars.length)]);\n        }\n        return mime.toString();\n    }\n\n    private static BomCharset detectCharsetFromBom(final ByteBuffer byteData) {\n        final Buffer buffer = byteData; // .mark and rewind used to return Buffer, now ByteBuffer, so cast for backward compat\n        buffer.mark();\n        byte[] bom = new byte[4];\n        if (byteData.remaining() >= bom.length) {\n            byteData.get(bom);\n            buffer.rewind();\n        }\n        if (bom[0] == 0x00 && bom[1] == 0x00 && bom[2] == (byte) 0xFE && bom[3] == (byte) 0xFF || // BE\n                bom[0] == (byte) 0xFF && bom[1] == (byte) 0xFE && bom[2] == 0x00 && bom[3] == 0x00) { // LE\n            return new BomCharset(\"UTF-32\", false); // and I hope it's on your system\n        } else if (bom[0] == (byte) 0xFE && bom[1] == (byte) 0xFF || // BE\n                bom[0] == (byte) 0xFF && bom[1] == (byte) 0xFE) {\n            return new BomCharset(\"UTF-16\", false); // in all Javas\n        } else if (bom[0] == (byte) 0xEF && bom[1] == (byte) 0xBB && bom[2] == (byte) 0xBF) {\n            return new BomCharset(\"UTF-8\", true); // in all Javas\n            // 16 and 32 decoders consume the BOM to determine be/le; utf-8 should be consumed here\n        }\n        return null;\n    }\n\n    private static class BomCharset {\n        private final String charset;\n        private final boolean offset;\n\n        public BomCharset(String charset, boolean offset) {\n            this.charset = charset;\n            this.offset = offset;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/EncryptUtil.java",
    "content": "package me.wizos.loread.utils;\n\nimport java.nio.charset.StandardCharsets;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\n/**\n * 加密工具类\n * @author by Wizos on 2020/2/26.\n */\npublic class EncryptUtil {\n    /**\n     * 将字符串转成MD5值\n     *\n     * @param string 字符串\n     * @return MD5 后的字符串\n     */\n    public static String MD5(String string) {\n        byte[] hash;\n        try {\n            hash = MessageDigest.getInstance(\"MD5\").digest(string.getBytes(StandardCharsets.UTF_8));\n        } catch (NoSuchAlgorithmException e) {\n            e.printStackTrace();\n            return null;\n        }\n        StringBuilder hex = new StringBuilder(hash.length * 2);\n        for (byte b : hash) {\n            if ((b & 0xFF) < 0x10) {\n                hex.append(\"0\");\n            }\n            hex.append(Integer.toHexString(b & 0xFF));\n        }\n        return hex.toString();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/FileUtil.java",
    "content": "package me.wizos.loread.utils;\n\nimport android.Manifest;\nimport android.app.Activity;\nimport android.content.pm.PackageManager;\nimport android.os.Environment;\nimport android.text.Html;\n\nimport androidx.core.app.ActivityCompat;\n\nimport com.google.gson.Gson;\nimport com.google.gson.reflect.TypeToken;\nimport com.socks.library.KLog;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.FileReader;\nimport java.io.FileWriter;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.channels.FileChannel;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.db.CoreDB;\n\n/**\n * @author Wizos on 2016/3/19.\n */\npublic class FileUtil {\n    //判断外部存储(SD卡)是否可以读写\n    public static boolean isExternalStorageWritable() {\n        String state = Environment.getExternalStorageState();\n        return Environment.MEDIA_MOUNTED.equals(state);\n    }\n\n    //判断外部存储是否至少可以读\n    public boolean isExternalStorageReadable() {\n        String state = Environment.getExternalStorageState();\n        return Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state);\n    }\n\n\n    public static void deleteHtmlDirList(ArrayList<String> fileNameInMD5List) {\n        String externalCacheDir = App.i().getUserFilesDir() + \"/cache/\";\n        for (String fileNameInMD5 : fileNameInMD5List) {\n//            KLog.e(\"删除文件：\" +  externalCacheDir + fileNameInMD5 );\n            deleteHtmlDir(new File(externalCacheDir + fileNameInMD5));\n        }\n    }\n\n\n    /**\n     * 递归删除应用下的缓存\n     *\n     * @param dir 需要删除的文件或者文件目录\n     * @return 文件是否删除\n     */\n    public static boolean deleteHtmlDir(File dir) {\n        if (dir.isDirectory()) {\n//            KLog.i( dir + \"是文件夹\");\n            File[] files = dir.listFiles();\n            for (File file : files) {\n                deleteHtmlDir(file);\n            }\n            return dir.delete(); // 删除目录\n        } else {\n//            KLog.i( dir + \"只是文件\");\n            return dir.delete(); // 删除文件\n        }\n    }\n\n\n    public static boolean moveFile(String srcFileName, String destFileName) {\n        File srcFile = new File(srcFileName);\n        //KLog.i(\"文件是否存在：\" + srcFile.exists() + destFileName);\n        if (!srcFile.exists() || !srcFile.isFile()) {\n            return false;\n        }\n\n        File destFile = new File(destFileName);\n        if (!destFile.getParentFile().exists()) {\n            destFile.getParentFile().mkdirs();\n        }\n        return srcFile.renameTo(destFile);\n    }\n\n    /**\n     * 移动目录\n     *\n     * @param srcDirName  源目录完整路径\n     * @param destDirName 目的目录完整路径\n     * @return 目录移动成功返回true，否则返回false\n     */\n    public static boolean moveDir(String srcDirName, String destDirName) {\n        //KLog.i(\"移动文件夹a\");\n        File srcDir = new File(srcDirName);\n        if (!srcDir.exists() || !srcDir.isDirectory()) {\n            return false;\n        }\n\n        File destDir = new File(destDirName);\n        if (!destDir.exists()) {\n            destDir.mkdirs();\n        }\n        //KLog.i(\"移动文件夹b\");\n        /**\n         * 如果是文件则移动，否则递归移动文件夹。删除最终的空源文件夹\n         * 注意移动文件夹时保持文件夹的树状结构\n         */\n        File[] sourceFiles = srcDir.listFiles();\n        for (File sourceFile : sourceFiles) {\n            if (sourceFile.isFile()) {\n                moveFile(sourceFile.getAbsolutePath(), destDir.getAbsolutePath() + File.separator + sourceFile.getName());\n            } else if (sourceFile.isDirectory()) {\n                moveDir(sourceFile.getAbsolutePath(), destDir.getAbsolutePath() + File.separator + sourceFile.getName());\n            }\n        }\n        return srcDir.delete();\n    }\n\n    public static void restore() {\n        Gson gson = new Gson();\n        String content;\n        Article tmp;\n\n        content = readFile(App.i().getUserFilesDir() + \"/config/articles-backup.json\");\n        List<Article> articles = gson.fromJson(content, new TypeToken<List<Article>>() {}.getType());\n        if (articles == null) {\n            return;\n        }\n        KLog.e(\"文豪A：\" + articles.size() );\n        List<Article> unreadArticles = new ArrayList<>(articles.size());\n        for (Article article : articles) {\n            tmp = CoreDB.i().articleDao().getById(App.i().getUser().getId(), article.getId());\n            if (tmp == null) {\n                continue;\n            }\n            tmp.setReadStatus(article.getReadStatus());\n            tmp.setSaveStatus(article.getSaveStatus());\n            unreadArticles.add(tmp);\n        }\n        CoreDB.i().articleDao().update(unreadArticles);\n    }\n\n    public static void backup() {\n        Gson gson = new Gson();\n        String content;\n        List<Article> articles = CoreDB.i().articleDao().getBackup(App.i().getUser().getId());\n        List<Article> backups = new ArrayList<>(articles.size());\n        Article tmp;\n        for (Article article : articles) {\n            tmp = new Article();\n            tmp.setId(article.getId());\n            tmp.setReadStatus(article.getReadStatus());\n            tmp.setSaveStatus(article.getSaveStatus());\n            backups.add(tmp);\n        }\n        content = gson.toJson(backups);\n        save(App.i().getUserFilesDir() + \"/config/articles-backup.json\", content);\n    }\n\n    public static void saveArticle(String dir, Article article) {\n        String title = getSaveableName(article.getTitle());\n        String filePathTitle = dir + title;\n        String html = ArticleUtil.getPageForSave(article, title);\n\n        String articleIdInMD5 = EncryptUtil.MD5(article.getId());\n        save(filePathTitle + \".html\", html);\n        KLog.e(\"保存文件夹：\" + filePathTitle + \" , \" + App.i().getUserFilesDir() + \"/cache/\" + articleIdInMD5 + \"/original\");\n        moveDir(App.i().getUserFilesDir() + \"/cache/\" + articleIdInMD5 + \"/original\", filePathTitle + \"_files\");\n    }\n\n\n    /**\n     * 处理文件名中的特殊字符和表情，用于保存为文件\n     *\n     * @param fileName 文件名\n     * @return 处理后的文件名\n     */\n    public static String getSaveableName(String fileName) {\n        // 因为有些title会用 html中的转义。所以这里要改过来\n        fileName = Html.fromHtml(fileName).toString();\n        fileName = SymbolUtil.filterEmoji(fileName);\n        fileName = SymbolUtil.filterUnsavedSymbol(fileName).trim();\n        if (StringUtils.isEmpty(fileName)) {\n            fileName = TimeUtil.format(System.currentTimeMillis(),\"yyyyMMddHHmmss\");\n        } else if (fileName.length() <= 2) {\n            fileName = fileName + TimeUtil.format(System.currentTimeMillis(),\"_yyyyMMddHHmmss\");\n        }\n        return fileName.trim();\n    }\n\n    public static boolean saveText(String filePath, String fileContent, boolean append) {\n        if (!isExternalStorageWritable()) {\n            return false;\n        }\n        File file = new File(filePath);\n\n        try {\n            if (file.exists()) {\n                if (!append) {\n                    return false;\n                }\n            } else {\n                File folder = file.getParentFile();\n                if (!folder.exists()) {\n                    folder.mkdirs();\n                }\n            }\n\n//            KLog.d(\"【】\" + file.toString() + \"--\"+ folder.toString());\n            FileWriter fileWriter = new FileWriter(file, append); //在 (file,false) 后者表示在 fileWriter 对文件再次写入时，是否会在该文件的结尾续写，true 是续写，false 是覆盖。\n            fileWriter.write(fileContent);\n            fileWriter.flush();  // 刷新该流中的缓冲。将缓冲区中的字符数据保存到目的文件中去。\n            fileWriter.close();  // 关闭此流。在关闭前会先刷新此流的缓冲区。在关闭后，再写入或者刷新的话，会抛IOException异常。\n            return true;\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return false;\n    }\n\n    public static void save(String filePath, String fileContent) {\n        if (!isExternalStorageWritable()) {\n            return;\n        }\n        File file = new File(filePath);\n        File folder = file.getParentFile();\n        try {\n            if (folder != null && !folder.exists()) {\n                folder.mkdirs();\n            }\n            KLog.i(\"保存规则：\" + file.toString());\n            FileWriter fileWriter = new FileWriter(file, false); //在 (file,false) 后者表示在 fileWriter 对文件再次写入时，是否会在该文件的结尾续写，true 是续写，false 是覆盖。\n            fileWriter.write(fileContent);\n            fileWriter.flush();  // 刷新该流中的缓冲。将缓冲区中的字符数据保存到目的文件中去。\n            fileWriter.close();  // 关闭此流。在关闭前会先刷新此流的缓冲区。在关闭后，再写入或者刷新的话，会抛IOException异常。\n        } catch (IOException e) {\n            KLog.e(\"保存错误\");\n            e.printStackTrace();\n        }\n    }\n\n    public static void save(File file, String fileContent) throws IOException {\n        File folder = file.getParentFile();\n        if (!folder.exists()) {\n            folder.mkdirs();\n        }\n        KLog.e(\"【】\" + file.toString() + \"--\" + folder.toString());\n        FileWriter fileWriter = new FileWriter(file, false); //在 (file,false) 后者表示在 fileWriter 对文件再次写入时，是否会在该文件的结尾续写，true 是续写，false 是覆盖。\n        fileWriter.write(fileContent);\n        fileWriter.flush();  // 刷新该流中的缓冲。将缓冲区中的字符数据保存到目的文件中去。\n        fileWriter.close();  // 关闭此流。在关闭前会先刷新此流的缓冲区。在关闭后，再写入或者刷新的话，会抛IOException异常。\n    }\n\n    public static String readFile(String filePath) {\n        return readFile(new File(filePath));\n    }\n    private static final int REQUEST_EXTERNAL_STORAGE = 1;\n    private static String[] PERMISSIONS_STORAGE = {\n            Manifest.permission.READ_EXTERNAL_STORAGE,\n            Manifest.permission.WRITE_EXTERNAL_STORAGE\n    };\n    /**\n     * 在对sd卡进行读写操作之前调用这个方法\n     * Checks if the app has permission to write to device storage\n     * If the app does not has permission then the user will be prompted to grant permissions\n     */\n    public static void verifyStoragePermissions(Activity activity) {\n        // Check if we have write permission\n        int permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);\n        if (permission != PackageManager.PERMISSION_GRANTED) {\n            // We don't have permission so prompt the user\n            ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);\n        }\n    }\n    public static String readFile(File file) {\n        String fileContent = \"\", temp = \"\";\n        if (!file.exists()) {\n            return fileContent;\n        }\n        try {\n            FileReader fileReader = new FileReader(file);\n            BufferedReader br = new BufferedReader(fileReader);//一行一行读取 。在电子书程序上经常会用到。\n            while ((temp = br.readLine()) != null) {\n                fileContent += temp; // +\"\\r\\n\"\n            }\n            fileReader.close();\n            br.close();\n        } catch (IOException f){\n            f.printStackTrace();\n        }\n        return fileContent;\n    }\n\n    public static String readCacheFilePath(String articleIdInMD5, String originalUrl) {\n        // 为了避免我自己来获取 FileNameExt 时，由于得到的结果是重复的而导致图片也获取到一致的。所以采用 base64 的方式加密 originalUrl，来保证唯一\n        String fileNameExt, filePath;\n        fileNameExt = UriUtil.guessFileNameExt(originalUrl);\n\n        // 推测该图片在保存时，由于src有问题，导致获取的文件名有重复时自动加上 hashCode 的机制\n        filePath = App.i().getUserFilesDir() + \"/cache/\" + articleIdInMD5 + \"/original/\" + originalUrl.hashCode() + \"_\" + fileNameExt;\n        if (new File(filePath).exists()) {\n            return filePath;\n        }\n\n        filePath = App.i().getUserFilesDir() + \"/cache/\" + articleIdInMD5 + \"/compressed/\" + fileNameExt;\n        if (new File(filePath).exists()) {\n            return filePath;\n        }\n\n        filePath = App.i().getUserFilesDir() + \"/cache/\" + articleIdInMD5 + \"/original/\" + fileNameExt;\n        if (new File(filePath).exists()) {\n            return filePath;\n        }\n\n        // 推测可能是svg格式的，该类文件必须有后缀名才能在webView中显示出来\n        filePath = App.i().getUserFilesDir() + \"/cache/\" + articleIdInMD5 + \"/original/\" + fileNameExt + \".svg\";\n        if (new File(filePath).exists()) {\n            return filePath;\n        }\n\n//        KLog.e(\"ImageBridge\", \"要读取的url：\" + originalUrl + \"    文件位置\" + filePath);\n        return null;\n    }\n\n\n    //文件拷贝\n    //要复制的目录下的所有非子目录(文件夹)文件拷贝\n    public static int copyFile(String fromFile, String toFile) {\n        try {\n            InputStream fosfrom = new FileInputStream(fromFile);\n            OutputStream fosto = new FileOutputStream(toFile);\n            byte bt[] = new byte[1024];\n            int c;\n            while ((c = fosfrom.read(bt)) > 0) {\n                fosto.write(bt, 0, c);\n            }\n            fosfrom.close();\n            fosto.close();\n            KLog.e(\"图片复制完成\" + fosfrom.available() + new File(fromFile).exists());\n            return 0;\n        } catch (Exception ex) {\n            KLog.e(\"报错\", ex);\n            ex.printStackTrace();\n            return -1;\n        }\n    }\n\n    private static boolean copyFile(File srcFile, File destFile) {\n        boolean result = false;\n        if (!isExternalStorageWritable()) {\n            return false;\n        }\n\n        if ((srcFile == null) || (destFile == null) || !srcFile.exists()) {\n            return false;\n        }\n\n\n        if (destFile.exists()) {\n//            dest.delete(); // delete file\n            return false;\n        } else if (!destFile.getParentFile().exists()) {\n            destFile.getParentFile().mkdirs();\n        }\n\n        FileChannel srcChannel = null;\n        FileChannel dstChannel = null;\n\n        try {\n            srcChannel = new FileInputStream(srcFile).getChannel();\n            dstChannel = new FileOutputStream(destFile).getChannel();\n            srcChannel.transferTo(0, srcChannel.size(), dstChannel);\n            result = true;\n        } catch (FileNotFoundException e) {\n            e.printStackTrace();\n            return result;\n        } catch (IOException e) {\n            e.printStackTrace();\n            return result;\n        }\n        try {\n            srcChannel.close();\n            dstChannel.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return result;\n    }\n\n\n    public static boolean copyFileToPictures(File srcFile) {\n        File loreadDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() + File.separator + \"知微\");\n        if (!loreadDir.exists()) {\n            loreadDir.mkdirs();\n        }\n        String fileName = srcFile.getName();\n        String suffix = \"\";\n        if (fileName.contains(\".\")) {\n            suffix = fileName.substring(fileName.lastIndexOf(\".\"));\n        }\n        if (suffix.length() > 5) {\n            suffix = getImageSuffix(srcFile);\n        }\n        File destFile = new File(loreadDir.getAbsolutePath() + File.separator + TimeUtil.format(System.currentTimeMillis(),\"yyyyMMdd_HHmmss\") + suffix);\n        return copyFile(srcFile, destFile);\n    }\n\n\n\n    private static String getImageSuffix(File imageFile) {\n        try {\n            return getImageSuffix(new FileInputStream(imageFile));\n        } catch (FileNotFoundException e) {\n            return \".jpg\";\n        }\n    }\n\n    private static String getImageSuffix(InputStream in) {\n        try {\n//            in.skip(9);//跳过前9个字节\n//            byte[] b = getBytes(in, 10);\n            byte[] b = new byte[10];\n            in.read(b, 0, 10); //读取文件中的内容到b[]数组,//读取 nums 个字节赋值给 b\n            in.close();\n            byte b0 = b[0];\n            byte b1 = b[1];\n            byte b2 = b[2];\n            byte b3 = b[3];\n            byte b6 = b[6];\n            byte b7 = b[7];\n            byte b8 = b[8];\n            byte b9 = b[9];\n            if (b0 == (byte) 'G' && b1 == (byte) 'I' && b2 == (byte) 'F') {\n                return \".gif\";\n            } else if (b1 == (byte) 'P' && b2 == (byte) 'N' && b3 == (byte) 'G') {\n                return \".png\";\n            } else if (b6 == (byte) 'J' && b7 == (byte) 'F' && b8 == (byte) 'I' && b9 == (byte) 'F') {\n                return \".jpg\";\n            } else if (b6 == (byte) 'E' && b7 == (byte) 'x' && b8 == (byte) 'i' && b9 == (byte) 'f') {\n                return \".jpg\";\n            } else {\n                return \".jpg\";\n            }\n        } catch (Exception e) {\n            return \".jpg\";\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/ImageUtil.java",
    "content": "package me.wizos.loread.utils;\n\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Canvas;\nimport android.graphics.Matrix;\nimport android.graphics.Paint;\nimport android.media.ThumbnailUtils;\nimport android.os.AsyncTask;\n\nimport com.socks.library.KLog;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.lang.ref.WeakReference;\n\n\n/**\n * Created by Wizos on 2018/12/22.\n */\npublic class ImageUtil {\n    public interface OnMergeListener {\n        /**\n         * Fired when a compression returns successfully, override to handle in your own code\n         */\n        void onSuccess();\n\n        /**\n         * Fired when a compression fails to complete, override to handle in your own code\n         */\n        void onError(Throwable e);\n    }\n\n    public static void mergeBitmap(Context context, final File bgFile, final OnMergeListener onMergeListener) {\n        mergeBitmap(new WeakReference<Context>(context), bgFile, onMergeListener);\n    }\n\n    public static void mergeBitmap(final WeakReference<Context> context, final File bgFile, final OnMergeListener onMergeListener) {\n        if (!bgFile.getAbsolutePath().toLowerCase().endsWith(\".gif\") || !bgFile.getAbsolutePath().toLowerCase().contains(\"/compressed/\")) {\n            onMergeListener.onSuccess();\n            return;\n        }\n        AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {\n            @Override\n            public void run() {\n                try {\n                    FileInputStream fis1 = new FileInputStream(bgFile);\n                    Bitmap bgBitmap = BitmapFactory.decodeStream(fis1).copy(Bitmap.Config.ARGB_8888, true);\n\n                    int shortSide = Math.min(bgBitmap.getWidth(), bgBitmap.getHeight());\n                    Bitmap fgBitmap = BitmapFactory.decodeStream(context.get().getAssets().open(\"image/gif_player.png\")).copy(Bitmap.Config.ARGB_8888, true);\n\n                    if (shortSide < fgBitmap.getWidth()) {\n                        onMergeListener.onSuccess();\n                        return;\n                    }\n                    Bitmap newBitmap = ImageUtil.mergeBitmap(bgBitmap, fgBitmap);\n\n                    //将合并后的bitmap3保存为png图片到本地\n                    FileOutputStream out = new FileOutputStream(bgFile.getAbsolutePath());\n                    newBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);\n\n                    out.close();\n\n                    bgBitmap.recycle();\n                    bgBitmap = null;\n                    fgBitmap.recycle();\n                    fgBitmap = null;\n                    newBitmap.recycle();\n                    newBitmap = null;\n                    onMergeListener.onSuccess();\n                } catch (Exception e) {\n\n                    e.printStackTrace();\n                    KLog.e(\"报错\");\n                    onMergeListener.onError(e);\n                }\n            }\n        });\n    }\n\n\n    /**\n     * 作者：青青河边踩\n     * 链接：https://www.jianshu.com/p/36fe123d973e\n     * 合成图片\n     */\n    public static Bitmap mergeBitmap(Bitmap bgBitmap, Bitmap fgBitmap) {\n//        BitmapFactory.Options options = new BitmapFactory.Options();\n//        //只读取图片，不加载到内存中\n//        options.inJustDecodeBounds = true;\n//        // isSampleSize是表示对图片的缩放程度，比如值为2图片的宽度和高度都变为以前的1/2\n//        // inSampleSize只能是2的次方，如计算结果是7会按4进行压缩，计算结果是15会按8进行压缩。\n//        options.inSampleSize = 1;\n\n        //以其中一张图片的大小作为画布的大小，或者也可以自己自定义\n        Bitmap newBitmap = Bitmap.createBitmap(bgBitmap);\n        //生成画布\n        Canvas canvas = new Canvas(newBitmap);\n\n        // 生成画笔\n        Paint paint = new Paint();\n        int w = bgBitmap.getWidth();\n        int h = bgBitmap.getHeight();\n        int w_2 = fgBitmap.getWidth();\n        int h_2 = fgBitmap.getHeight();\n\n        float scale = (w * 0.2f) / w_2;\n        Matrix matrix = new Matrix();\n        matrix.postScale(scale, scale);\n        fgBitmap = Bitmap.createBitmap(fgBitmap, 0, 0, w_2, h_2, matrix, true);\n\n        // 设置第二张图片的位置\n        canvas.drawBitmap(fgBitmap, (w - fgBitmap.getWidth()) / 2, (h - fgBitmap.getHeight()) / 2, paint);\n        canvas.save(); // Canvas.ALL_SAVE_FLAG\n//        bgBitmap.recycle();\n//        bgBitmap = null;\n//        fgBitmap.recycle();\n//        fgBitmap = null;\n        // 存储新合成的图片\n        canvas.restore();\n        return newBitmap;\n    }\n\n\n    public static void genPic(final File file, final File fileNew) {\n        AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {\n            @Override\n            public void run() {\n                try {\n//                    FileInputStream fileInputStream = new FileInputStream(App.i().getExUserFilesDir() + \"/compressed/pic.jpg\");\n\n                    Bitmap newBitmap = getThumbnail(file, 1080);\n\n                    //将合并后的bitmap3保存为png图片到本地\n                    FileOutputStream out = new FileOutputStream(fileNew);\n                    newBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);\n                    KLog.e(\"成功获取 getThumbnail\");\n                } catch (Exception e) {\n                    KLog.e(\"报错\");\n                    e.printStackTrace();\n                }\n            }\n        });\n    }\n//        作者：呵呵瓤儿\n//        链接：https://www.jianshu.com/p/cc61ea00f768\n//        來源：简书\n//        简书著作权归作者所有，任何形式的转载都请联系作者获得授权并注明出处。\n\n    /**\n     * 这样得到的图片会比原图体积大3倍\n     *\n     * @param file\n     * @param screenWidth\n     * @return\n     * @throws IOException\n     */\n    public static Bitmap getThumbnail(File file, int screenWidth) throws IOException {\n        BitmapFactory.Options options = new BitmapFactory.Options();\n        //只读取图片，不加载到内存中\n        options.inJustDecodeBounds = true;\n        options.inSampleSize = 1;\n\n        BitmapFactory.decodeStream(new FileInputStream(file), null, options);\n        int imgWidth = options.outWidth;\n        int imgHeight = options.outHeight;\n\n        KLog.e(\"宽高1=\" + imgWidth + \"  \" + imgHeight);\n        // 将长宽变为偶数\n        imgWidth = imgWidth % 2 == 1 ? imgWidth + 1 : imgWidth;\n        imgHeight = imgHeight % 2 == 1 ? imgHeight + 1 : imgHeight;\n        screenWidth = screenWidth % 2 == 1 ? screenWidth + 1 : screenWidth;\n\n        // 如果图片宽度大于屏幕宽度，则生成缩略图\n        if (imgWidth > screenWidth) {\n            imgHeight = (int) (((float) screenWidth) / ((float) imgWidth) * imgHeight);\n            imgWidth = screenWidth;\n        }\n        return ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(file.getAbsolutePath()), imgWidth, imgHeight);\n    }\n\n    /**\n     * 未检验过\n     */\n//    作者：dream_monkey\n//    原文：https://blog.csdn.net/dream_monkey/article/details/51461622\n    public static Bitmap adjustImage(String absolutePath, int screenWidth, int screenHeight) {\n        BitmapFactory.Options opt = new BitmapFactory.Options();\n        // 这个isjustdecodebounds很重要\n        opt.inJustDecodeBounds = true;\n        BitmapFactory.decodeFile(absolutePath, opt);\n\n        // 获取到这个图片的原始宽度和高度\n        int picWidth = opt.outWidth;\n//        int picHeight = opt.outHeight;\n\n        // 将长宽变为偶数\n        picWidth = picWidth % 2 == 1 ? picWidth + 1 : picWidth;\n        screenWidth = screenWidth % 2 == 1 ? screenWidth + 1 : screenWidth;\n//        picHeight = picHeight % 2 == 1 ? picHeight + 1 : picHeight;\n//        screenHeight = screenHeight % 2 == 1 ? screenHeight + 1 : screenHeight;\n\n        // isSampleSize是表示对图片的缩放程度，比如值为2图片的宽度和高度都变为以前的1/2\n        // inSampleSize只能是2的次方，如计算结果是7会按4进行压缩，计算结果是15会按8进行压缩。\n        opt.inSampleSize = 1;\n        // 根据屏的大小和图片大小计算出缩放比例\n        if (picWidth > screenWidth)\n            opt.inSampleSize = picWidth / screenWidth;\n\n//        if (picWidth > picHeight) {\n//            if (picWidth > screenWidth)\n//                opt.inSampleSize = picWidth / screenWidth;\n//        } else {\n//            if (picHeight > screenHeight)\n//                opt.inSampleSize = picHeight / screenHeight;\n//        }\n\n        // 这次再真正地生成一个有像素的，经过缩放了的bitmap\n        opt.inJustDecodeBounds = false;\n        return BitmapFactory.decodeFile(absolutePath, opt);\n    }\n\n    public static String getImageType(File srcFilePath) {\n        FileInputStream imgFile;\n        byte[] b = new byte[10];\n        int l = -1;\n        try {\n            imgFile = new FileInputStream(srcFilePath);\n            l = imgFile.read(b);\n            imgFile.close();\n        } catch (Exception e) {\n            return null;\n        }\n        if (l == 10) {\n            byte b0 = b[0];\n            byte b1 = b[1];\n            byte b2 = b[2];\n            byte b3 = b[3];\n            byte b6 = b[6];\n            byte b7 = b[7];\n            byte b8 = b[8];\n            byte b9 = b[9];\n            if (b0 == (byte) 'G' && b1 == (byte) 'I' && b2 == (byte) 'F') {\n                return \"gif\";\n            } else if (b1 == (byte) 'P' && b2 == (byte) 'N' && b3 == (byte) 'G') {\n                return \"png\";\n            } else if (b6 == (byte) 'J' && b7 == (byte) 'F' && b8 == (byte) 'I' && b9 == (byte) 'F') {\n                return \"jpg\";\n            } else {\n                return null;\n            }\n        } else {\n            return null;\n        }\n    }\n\n    public static boolean isImg(File file) {\n        try {\n            FileInputStream is = new FileInputStream(file);\n            byte[] src = new byte[28];\n            is.read(src, 0, 28);\n            StringBuilder stringBuilder = new StringBuilder(\"\");\n            for (byte b : src) {\n                int v = b & 0xFF;\n                String hv = Integer.toHexString(v).toUpperCase();\n                if (hv.length() < 2) {\n                    stringBuilder.append(0);\n                }\n                stringBuilder.append(hv);\n            }\n            ImgFileType[] fileTypes = ImgFileType.values();\n            for (ImgFileType fileType : fileTypes) {\n                if (stringBuilder.toString().startsWith(fileType.getValue())) {\n                    return true;\n                }\n            }\n        }catch (IOException e){\n            return false;\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/ImgFileType.java",
    "content": "package me.wizos.loread.utils;\n\n/**\n * Created by jiangzeyin on 2017/3/15.\n */\npublic enum ImgFileType {\n\n    /**\n     * JPEG,JPG\n     */\n    JPEG(\"FFD8FF\", \"jpg\"),\n\n    /**\n     * PNG\n     */\n    PNG(\"89504E47\", \"png\"),\n\n    /**\n     * GIF\n     */\n    GIF(\"47494638\", \"gif\"),\n\n    /**\n     * TIFF\n     */\n    TIFF(\"49492A00\"),\n\n    /**\n     * Windows bitmap\n     */\n    BMP(\"424D\"),\n\n\n    WEBP(\"52494646\"),\n\n    /**\n     * svg，还有一种是xml文件头\n     */\n    SVG(\"3C73766720\");\n\n    private String value = \"\";\n    private String ext = \"\";\n\n    ImgFileType(String value) {\n        this.value = value;\n    }\n\n    ImgFileType(String value, String ext) {\n        this(value);\n        this.ext = ext;\n    }\n\n    public String getExt() {\n        return ext;\n    }\n\n    public String getValue() {\n        return value;\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/NetworkUtil.java",
    "content": "package me.wizos.loread.utils;\n\nimport android.content.Context;\nimport android.net.ConnectivityManager;\nimport android.net.NetworkInfo;\n\nimport me.wizos.loread.App;\n\n\n/**\n * @author Wizos on 2018-06-09\n * @version 1.0\n */\npublic class NetworkUtil {\n    public static final int NETWORK_NONE = 0;\n    public static final int NETWORK_MOBILE = 1;\n    public static final int NETWORK_WIFI = 2;\n    private static int THE_NETWORK = 0;\n\n    /**\n     * 判断网络的状态\n     */\n    public static int getNetWorkState() {\n        ConnectivityManager connectivityManager = (ConnectivityManager) App.i().getSystemService(Context.CONNECTIVITY_SERVICE);\n        NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();\n        if (networkInfo != null && networkInfo.isConnected()) {\n            if (networkInfo.getType() == (ConnectivityManager.TYPE_WIFI)) {\n                THE_NETWORK = NETWORK_WIFI;\n            } else if (networkInfo.getType() == (ConnectivityManager.TYPE_MOBILE)) {\n                THE_NETWORK = NETWORK_MOBILE;\n            }\n        } else {\n            THE_NETWORK = NETWORK_NONE;\n        }\n        return THE_NETWORK;\n    }\n\n    public static boolean isNetworkAvailable() {\n        return THE_NETWORK != NETWORK_NONE;\n    }\n\n    public static boolean isWiFiUsed() {\n        return THE_NETWORK == NETWORK_WIFI;\n    }\n\n    public static void setTheNetwork(int network){\n        THE_NETWORK = network;\n    }\n\n    public static boolean canDownImg() {\n        if (!isNetworkAvailable()) {\n            return false;\n        }\n        // 开启了仅Wifi情况下载，但是不处于wifi状态\n        return !(App.i().getUser().isDownloadImgOnlyWifi() && !isWiFiUsed());\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/RGB.java",
    "content": "package me.wizos.loread.utils;\n\n/**\n * Created by Wizos on 2018/7/10.\n */\n\npublic class RGB {\n    int r;\n    int g;\n    int b;\n\n    public RGB(int r, int g, int b) {\n        this.r = r;\n        this.g = g;\n        this.b = b;\n    }\n\n    @Override\n    public String toString() {\n        return \"RGB{\" +\n                \"r=\" + r +\n                \", g=\" + g +\n                \", b=\" + b +\n                '}';\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/ScreenUtil.java",
    "content": "package me.wizos.loread.utils;\n\nimport android.app.Activity;\nimport android.app.ActivityGroup;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.Rect;\nimport android.os.Build;\nimport android.util.DisplayMetrics;\nimport android.util.Log;\nimport android.util.TypedValue;\nimport android.view.Display;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.WindowManager;\n\nimport androidx.annotation.DimenRes;\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.R;\n\n\n/**\n * 屏幕工具\n *\n * @author Wizos on 2016/2/13.\n */\npublic class ScreenUtil {\n\n    /**\n     * 从 R.dimen 文件中获取到数值，再根据手机的分辨率转成为 px(像素)\n     */\n    public static int get2Px(Context context, @DimenRes int id) {\n        final float scale = context.getResources().getDisplayMetrics().density;\n        final float dpValue = (int) context.getResources().getDimension(id);\n        return (int) (dpValue * scale + 0.5f);\n    }\n\n    public static int getDimen(Context context, @DimenRes int id) {\n        return (int) context.getResources().getDimension(id);\n    }\n\n    /**\n     * 根据手机的分辨率从 dp 的单位 转成为 px(像素)\n     */\n    public static int dp2px(Context context, float dpValue) {\n        final float density = context.getResources().getDisplayMetrics().density;\n        return (int) (dpValue * density + 0.5f);\n    }\n\n    public static int dp2Px2(int dp) {\n        // 屏幕密度,系统源码注释不推荐使用\n        DisplayMetrics displayMetrics = App.i().getResources().getDisplayMetrics();\n        return Math.round(dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT));\n    }\n\n    /**\n     * 根据手机的分辨率从 px(像素) 的单位 转成为 dp\n     */\n    public static int px2dp(Context context, float pxValue) {\n        final float scale = context.getResources().getDisplayMetrics().density;\n        return (int) (pxValue / scale + 0.5f);\n    }\n\n\n    public static int sp2px(Context context, float spValue) {\n        final float scaledDensity = context.getResources().getDisplayMetrics().scaledDensity;\n        return (int) (spValue * scaledDensity + 0.5f);\n    }\n\n    /**\n     * dp转换成px\n     *\n     * @param dp dp\n     * @return px值\n     */\n    public static int dp2px(float dp) {\n        final float scale = App.i().getResources().getDisplayMetrics().density;\n        return (int) (dp * scale + 0.5f);\n    }\n\n    /**\n     * 获取屏幕的宽度\n     *\n     * @param context\n     * @return\n     */\n    public static int getScreenWidth(Context context) {\n        WindowManager manager = (WindowManager) context\n                .getSystemService(Context.WINDOW_SERVICE);\n        Display display = manager.getDefaultDisplay();\n        return display.getWidth();\n    }\n\n    /**\n     * 获取屏幕的高度\n     *\n     * @param context\n     * @return\n     */\n    public static int getScreenHeight(Context context) {\n        WindowManager manager = (WindowManager) context\n                .getSystemService(Context.WINDOW_SERVICE);\n        Display display = manager.getDefaultDisplay();\n        return display.getHeight();\n    }\n\n\n    /**\n     * 获取屏幕内容高度\n     *\n     * @param activity\n     * @return\n     */\n    public static int getScreenHeight2(Activity activity) {\n        DisplayMetrics dm = new DisplayMetrics();\n        activity.getWindowManager().getDefaultDisplay().getMetrics(dm);\n        int statusHeight = 0;\n        int resourceId = activity.getResources().getIdentifier(\"status_bar_height\", \"dimen\", \"android\");\n        if (resourceId > 0) {\n            statusHeight = activity.getResources().getDimensionPixelSize(resourceId);\n        }\n        return dm.heightPixels - statusHeight;\n    }\n\n    /**\n     * 获得状态栏的高度\n     *\n     * @param context\n     * @return mStatusHeight\n     */\n    private static int mStatusHeight = -1;\n\n    public static int getStatusHeight(Context context) {\n        if (mStatusHeight != -1) {\n            return mStatusHeight;\n        }\n        try {\n            int resourceId = context.getResources().getIdentifier(\"status_bar_height\", \"dimen\", \"android\");\n            if (resourceId > 0) {\n                mStatusHeight = context.getResources().getDimensionPixelSize(resourceId);\n            }\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return mStatusHeight;\n    }\n\n\n    /**\n     * 获取当前屏幕截图，不包含状态栏\n     *\n     * @param activity\n     * @return bp\n     */\n    public static Bitmap snapShotWithoutStatusBar(Activity activity) {\n        View view = activity.getWindow().getDecorView();\n        view.setDrawingCacheEnabled(true);\n        view.buildDrawingCache();\n        Bitmap bmp = view.getDrawingCache();\n        if (bmp == null) {\n            return null;\n        }\n        Rect frame = new Rect();\n        activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);\n        int statusBarHeight = frame.top;\n        Bitmap bp = Bitmap.createBitmap(bmp, 0, statusBarHeight, bmp.getWidth(), bmp.getHeight() - statusBarHeight);\n        view.destroyDrawingCache();\n        view.setDrawingCacheEnabled(false);\n        return bp;\n    }\n\n    /**\n     * 获取actionbar的像素高度，默认使用android官方兼容包做actionbar兼容\n     *\n     * @return\n     */\n    public static int getActionBarHeight(Context context) {\n        int actionBarHeight = 0;\n        if (context instanceof AppCompatActivity && ((AppCompatActivity) context).getSupportActionBar() != null) {\n            Log.d(\"isAppCompatActivity\", \"==AppCompatActivity\");\n            actionBarHeight = ((AppCompatActivity) context).getSupportActionBar().getHeight();\n        } else if (context instanceof Activity && ((Activity) context).getActionBar() != null) {\n            Log.d(\"isActivity\", \"==Activity\");\n            actionBarHeight = ((Activity) context).getActionBar().getHeight();\n        } else if (context instanceof ActivityGroup) {\n            Log.d(\"ActivityGroup\", \"==ActivityGroup\");\n            if (((ActivityGroup) context).getCurrentActivity() instanceof AppCompatActivity && ((AppCompatActivity) ((ActivityGroup) context).getCurrentActivity()).getSupportActionBar() != null) {\n                actionBarHeight = ((AppCompatActivity) ((ActivityGroup) context).getCurrentActivity()).getSupportActionBar().getHeight();\n            } else if (((ActivityGroup) context).getCurrentActivity() instanceof Activity && ((ActivityGroup) context).getCurrentActivity().getActionBar() != null) {\n                actionBarHeight = ((ActivityGroup) context).getCurrentActivity().getActionBar().getHeight();\n            }\n        }\n        if (actionBarHeight != 0) {\n            return actionBarHeight;\n        }\n        final TypedValue tv = new TypedValue();\n        if (context.getTheme().resolveAttribute(R.attr.actionBarSize, tv, true)) {\n            if (context.getTheme().resolveAttribute(R.attr.actionBarSize, tv, true)) {\n                actionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics());\n            }\n        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {\n            if (context.getTheme().resolveAttribute(R.attr.actionBarSize, tv, true)) {\n                actionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics());\n            }\n        } else {\n            if (context.getTheme().resolveAttribute(R.attr.actionBarSize, tv, true)) {\n                actionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics());\n            }\n        }\n        Log.d(\"actionBarHeight\", \"====\" + actionBarHeight);\n        return actionBarHeight;\n    }\n\n\n    /**\n     * 设置view margin\n     *\n     * @param v\n     * @param l\n     * @param t\n     * @param r\n     * @param b\n     */\n    public static void setMargins(View v, int l, int t, int r, int b) {\n        if (v.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {\n            ViewGroup.MarginLayoutParams p = (ViewGroup.MarginLayoutParams) v.getLayoutParams();\n            p.setMargins(l, t, r, b);\n            v.requestLayout();\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/ScriptUtil.java",
    "content": "package me.wizos.loread.utils;\n\nimport com.socks.library.KLog;\n\nimport javax.script.Bindings;\nimport javax.script.ScriptEngine;\nimport javax.script.ScriptEngineManager;\nimport javax.script.ScriptException;\n\n\npublic class ScriptUtil {\n    private static ScriptUtil instance;\n    private static ScriptEngine engine;\n    private ScriptUtil() { }\n\n    public static synchronized ScriptUtil init() {\n        if (instance == null) {\n            synchronized (ScriptUtil.class) {\n                if (instance == null) {\n                    instance = new ScriptUtil();\n                    engine =  new ScriptEngineManager().getEngineByName(\"rhino\");\n                }\n            }\n        }\n        return instance;\n    }\n\n    public static ScriptUtil i(){\n        if( instance == null ){\n            init();\n        }\n        return instance;\n    }\n\n    public boolean eval(String js, Bindings bindings){\n        try {\n            engine.eval(js, bindings);\n            return true;\n        } catch (ScriptException e) {\n            KLog.e(\"脚本执行错误\" + e.getMessage() + \",\" +e.getFileName()  + \",\"+ e.getColumnNumber()  + \",\" + e.getLineNumber() );\n            e.printStackTrace();\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/SnackbarUtil.java",
    "content": "package me.wizos.loread.utils;\n\nimport android.annotation.TargetApi;\nimport android.graphics.drawable.ColorDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.graphics.drawable.GradientDrawable;\nimport android.os.Build;\nimport android.util.Log;\nimport android.view.Gravity;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.Button;\nimport android.widget.FrameLayout;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.StringRes;\nimport androidx.coordinatorlayout.widget.CoordinatorLayout;\nimport androidx.legacy.widget.Space;\n\nimport com.google.android.material.snackbar.Snackbar;\n\nimport java.lang.ref.WeakReference;\n\nimport me.wizos.loread.R;\n\n/**\n * Snackbar工具类\n * 功能:\n * 1:设置Snackbar显示时间长短\n * 1.1:Snackbar.LENGTH_SHORT       {@link SnackbarUtil#Short(View, String)}\n * 1.2:Snackbar.LENGTH_LONG        {@link SnackbarUtil#Long(View, String)}\n * 1.3:Snackbar.LENGTH_INDEFINITE  {@link SnackbarUtil#Indefinite(View, String)}\n * 1.4:CUSTOM                      {@link SnackbarUtil#Custom(View, String, int)}\n * 2:设置Snackbar背景颜色\n * 2.1:color_info      {@link SnackbarUtil#info()}\n * 2.2:color_confirm   {@link SnackbarUtil#confirm()}\n * 2.3:color_warning   {@link SnackbarUtil#warning()}\n * 2.4:color_danger    {@link SnackbarUtil#danger()}\n * 2.5:CUSTOM          {@link SnackbarUtil#backColor(int)}\n * 3:设置TextView(@+id/snackbar_text)的文字颜色\n * {@link SnackbarUtil#messageColor(int)}\n * 4:设置Button(@+id/snackbar_action)的文字颜色\n * {@link SnackbarUtil#actionColor(int)}\n * 5:设置Snackbar背景的透明度\n * {@link SnackbarUtil#alpha(float)}\n * 6:设置Snackbar显示的位置\n * {@link SnackbarUtil#gravityFrameLayout(int)}\n * {@link SnackbarUtil#gravityCoordinatorLayout(int)}\n * 6.1:Gravity.TOP;\n * 6.2:Gravity.BOTTOM;\n * 6.3:Gravity.CENTER;\n * 7:设置Button(@+id/snackbar_action)文字内容 及 点击监听\n * {@link SnackbarUtil#setAction(int, View.OnClickListener)}\n * {@link SnackbarUtil#setAction(CharSequence, View.OnClickListener)}\n * 8:设置Snackbar展示完成 及 隐藏完成 的监听\n * {@link SnackbarUtil#setCallback(Snackbar.Callback)}\n * 9:设置TextView(@+id/snackbar_text)左右两侧的图片\n * {@link SnackbarUtil#leftAndRightDrawable(Drawable, Drawable)}\n * {@link SnackbarUtil#leftAndRightDrawable(Integer, Integer)}\n * 10:设置TextView(@+id/snackbar_text)中文字的对齐方式\n * 默认效果就是居左对齐\n * {@link SnackbarUtil#messageCenter()}   居中对齐\n * {@link SnackbarUtil#messageRight()}    居右对齐\n * 注意:这两个方法要求SDK>=17.{@link View#setTextAlignment(int)}\n * 本来想直接设置Gravity,经试验发现在 TextView(@+id/snackbar_text)上,design_layout_snackbar_include.xml\n * 已经设置了android:textAlignment=\"viewStart\",单纯设置Gravity是无效的.\n * TEXT_ALIGNMENT_GRAVITY:{@link View#TEXT_ALIGNMENT_GRAVITY}\n * 11:向Snackbar布局中添加View(Google不建议,复杂的布局应该使用DialogFragment进行展示)\n * {@link SnackbarUtil#addView(int, int)}\n * {@link SnackbarUtil#addView(View, int)}\n * 注意:使用addView方法的时候要注意新加布局的大小和Snackbar内文字长度，Snackbar过大或过于花哨了可不好看\n * 12:设置Snackbar布局的外边距\n * {@link SnackbarUtil#margins(int)}\n * {@link SnackbarUtil#margins(int, int, int, int)}\n * 注意:经试验发现,调用margins后再调用 gravityFrameLayout,则margins无效.\n * 为保证margins有效,应该先调用 gravityFrameLayout,在 show() 之前调用 margins\n * SnackbarUtil.Long(bt9,\"设置Margin值\").backColor(0XFF330066).gravityFrameLayout(Gravity.TOP).margins(20,40,60,80).show();\n * 13:设置Snackbar布局的圆角半径值\n * {@link SnackbarUtil#radius(float)}\n * 14:设置Snackbar布局的圆角半径值及边框颜色及边框宽度\n * {@link SnackbarUtil#radius(int, int, int)}\n * 15:设置Snackbar显示在指定View的上方\n * {@link SnackbarUtil#above(View, int, int, int)}\n * 注意:\n * 1:此方法实际上是 {@link SnackbarUtil#gravityFrameLayout(int)}和{@link SnackbarUtil#margins(int, int, int, int)}的结合.\n * 不可与 {@link SnackbarUtil#margins(int, int, int, int)} 混用.\n * 2:暂时仅仅支持单行Snackbar,因为方法中涉及的{@link SnackbarUtil#calculateSnackBarHeight()}暂时仅支持单行Snackbar高度计算.\n * 16:设置Snackbar显示在指定View的下方\n * {@link SnackbarUtil#bellow(View, int, int, int)}\n * 注意:同15\n * 参考:\n * //写的很好的Snackbar源码分析\n * http://blog.csdn.net/wuyuxing24/article/details/51220415\n * //借鉴了作者部分写法,自定义显示时间 及 向Snackbar中添加View\n * http://www.jianshu.com/p/cd1e80e64311\n * //借鉴了作者部分写法,4种类型的背景色 及 方法调用的便捷性\n * http://www.jianshu.com/p/e3c82b98f151\n * //大神'工匠若水'的文章'Android应用坐标系统全面详解',用于计算Snackbar显示的精确位置\n * http://blog.csdn.net/yanbober/article/details/50419117\n * 示例:\n * 在Activity中:\n * int total = 0;\n * int[] locations = new int[2];\n * getWindow().findViewById(android.R.id.content).getLocationInWindow(locations);\n * total = locations[1];\n * SnackbarUtil.Custom(bt_multimethods,\"10s+左右drawable+背景色+圆角带边框+指定View下方\",1000*10)\n * .leftAndRightDrawable(R.mipmap.i10,R.mipmap.i11)\n * .backColor(0XFF668899)\n * .radius(16,1,Color.BLUE)\n * .bellow(bt_margins,total,16,16)\n * .show();\n * 作者:幻海流心\n * 邮箱:wall0920@163.com\n * 2016/11/2 13:56\n */\n\npublic class SnackbarUtil {\n    //设置Snackbar背景颜色\n    private static final int color_info = 0XFF2094F3;\n    private static final int color_confirm = 0XFF4CB04E;\n    private static final int color_warning = 0XFFFEC005;\n    private static final int color_danger = 0XFFF44336;\n    //工具类当前持有的Snackbar实例\n    private static WeakReference<Snackbar> snackbarWeakReference;\n\n    private SnackbarUtil() {\n        throw new RuntimeException(\"禁止无参创建实例\");\n    }\n\n    private SnackbarUtil(@Nullable WeakReference<Snackbar> snackbarWeakReference) {\n        SnackbarUtil.snackbarWeakReference = snackbarWeakReference;\n    }\n\n    /**\n     * 获取 mSnackbar\n     *\n     * @return\n     */\n    public Snackbar getSnackbar() {\n        if (snackbarWeakReference != null && snackbarWeakReference.get() != null) {\n            return snackbarWeakReference.get();\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * 初始化Snackbar实例\n     * 展示时间:Snackbar.LENGTH_SHORT\n     *\n     * @param view    该view必须能找到父布局才能用\n     * @param message\n     * @return\n     */\n    public static SnackbarUtil Short(View view, String message) {\n        /*\n        <view xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          class=\"android.support.design.widget.Snackbar$SnackbarLayout\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"wrap_content\"\n          android:layout_gravity=\"bottom\"\n          android:theme=\"@style/ThemeOverlay.AppCompat.Dark\"\n          style=\"@style/Widget.Design.Snackbar\" />\n        <style name=\"Widget.Design.Snackbar\" parent=\"android:Widget\">\n            <item name=\"android:minWidth\">@dimen/design_snackbar_min_width</item>\n            <item name=\"android:maxWidth\">@dimen/design_snackbar_max_width</item>\n            <item name=\"android:background\">@drawable/design_snackbar_background</item>\n            <item name=\"android:paddingLeft\">@dimen/design_snackbar_padding_horizontal</item>\n            <item name=\"android:paddingRight\">@dimen/design_snackbar_padding_horizontal</item>\n            <item name=\"elevation\">@dimen/design_snackbar_elevation</item>\n            <item name=\"maxActionInlineWidth\">@dimen/design_snackbar_action_inline_max_width</item>\n        </style>\n        <shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n            android:shape=\"rectangle\">\n            <corners android:radius=\"@dimen/design_snackbar_background_corner_radius\"/>\n            <solid android:color=\"@color/design_snackbar_background_color\"/>\n        </shape>\n        <color name=\"design_snackbar_background_color\">#323232</color>\n        */\n        return new SnackbarUtil(new WeakReference<Snackbar>(Snackbar.make(view, message, Snackbar.LENGTH_SHORT))).backColor(0XFF323232);\n    }\n\n    /**\n     * 初始化Snackbar实例\n     * 展示时间:Snackbar.LENGTH_LONG\n     *\n     * @param view    该view必须能找到父布局才能用\n     * @param message\n     * @return\n     */\n    public static SnackbarUtil Long(View view, String message) {\n        return new SnackbarUtil(new WeakReference<Snackbar>(Snackbar.make(view, message, Snackbar.LENGTH_LONG))).backColor(0XFF323232);\n    }\n    public static SnackbarUtil Long(View view,View anchorView, String message) {\n        return new SnackbarUtil(new WeakReference<Snackbar>(Snackbar.make(view, message, Snackbar.LENGTH_LONG).setAnchorView(anchorView))).backColor(0XFF323232);\n    }\n\n    /**\n     * 初始化Snackbar实例\n     * 展示时间:Snackbar.LENGTH_INDEFINITE\n     *\n     * @param view    该view必须能找到父布局才能用\n     * @param message\n     * @return\n     */\n    public static SnackbarUtil Indefinite(View view, String message) {\n        return new SnackbarUtil(new WeakReference<Snackbar>(Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE))).backColor(0XFF323232);\n    }\n\n    /**\n     * 初始化Snackbar实例\n     * 展示时间:duration 毫秒\n     *\n     * @param view     该view必须能找到父布局才能用\n     * @param message\n     * @param duration 展示时长(毫秒)\n     * @return\n     */\n    public static SnackbarUtil Custom(View view, String message, int duration) {\n        return new SnackbarUtil(new WeakReference<Snackbar>(Snackbar.make(view, message, Snackbar.LENGTH_SHORT).setDuration(duration))).backColor(0XFF323232);\n    }\n\n    /**\n     * 设置mSnackbar背景色为  color_info\n     */\n    public SnackbarUtil info() {\n        if (getSnackbar() != null) {\n            getSnackbar().getView().setBackgroundColor(color_info);\n        }\n        return this;\n    }\n\n    /**\n     * 设置mSnackbar背景色为  color_confirm\n     */\n    public SnackbarUtil confirm() {\n        if (getSnackbar() != null) {\n            getSnackbar().getView().setBackgroundColor(color_confirm);\n        }\n        return this;\n    }\n\n    /**\n     * 设置Snackbar背景色为   color_warning\n     */\n    public SnackbarUtil warning() {\n        if (getSnackbar() != null) {\n            getSnackbar().getView().setBackgroundColor(color_warning);\n        }\n        return this;\n    }\n\n    /**\n     * 设置Snackbar背景色为   color_warning\n     */\n    public SnackbarUtil danger() {\n        if (getSnackbar() != null) {\n            getSnackbar().getView().setBackgroundColor(color_danger);\n        }\n        return this;\n    }\n\n    /**\n     * 设置Snackbar背景色\n     *\n     * @param backgroundColor\n     */\n    public SnackbarUtil backColor(@ColorInt int backgroundColor) {\n        if (getSnackbar() != null) {\n            getSnackbar().getView().setBackgroundColor(backgroundColor);\n        }\n        return this;\n    }\n\n    /**\n     * 设置TextView(@+id/snackbar_text)的文字颜色\n     *\n     * @param messageColor\n     */\n    public SnackbarUtil messageColor(@ColorInt int messageColor) {\n        if (getSnackbar() != null) {\n            ((TextView) getSnackbar().getView().findViewById(R.id.snackbar_text)).setTextColor(messageColor);\n        }\n        return this;\n    }\n\n    /**\n     * 设置Button(@+id/snackbar_action)的文字颜色\n     *\n     * @param actionTextColor\n     */\n    public SnackbarUtil actionColor(@ColorInt int actionTextColor) {\n        if (getSnackbar() != null) {\n            ((Button) getSnackbar().getView().findViewById(R.id.snackbar_action)).setTextColor(actionTextColor);\n        }\n        return this;\n    }\n\n    /**\n     * 设置   Snackbar背景色 + TextView(@+id/snackbar_text)的文字颜色 + Button(@+id/snackbar_action)的文字颜色\n     *\n     * @param backgroundColor\n     * @param messageColor\n     * @param actionTextColor\n     */\n    public SnackbarUtil colors(@ColorInt int backgroundColor, @ColorInt int messageColor, @ColorInt int actionTextColor) {\n        if (getSnackbar() != null) {\n            getSnackbar().getView().setBackgroundColor(backgroundColor);\n            ((TextView) getSnackbar().getView().findViewById(R.id.snackbar_text)).setTextColor(messageColor);\n            ((Button) getSnackbar().getView().findViewById(R.id.snackbar_action)).setTextColor(actionTextColor);\n        }\n        return this;\n    }\n\n    /**\n     * 设置Snackbar 背景透明度\n     *\n     * @param alpha\n     * @return\n     */\n    public SnackbarUtil alpha(float alpha) {\n        if (getSnackbar() != null) {\n            alpha = alpha >= 1.0f ? 1.0f : (alpha <= 0.0f ? 0.0f : alpha);\n            getSnackbar().getView().setAlpha(alpha);\n        }\n        return this;\n    }\n\n    /**\n     * 设置Snackbar显示的位置\n     *\n     * @param gravity\n     */\n    public SnackbarUtil gravityFrameLayout(int gravity) {\n        if (getSnackbar() != null) {\n            FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(getSnackbar().getView().getLayoutParams().width, getSnackbar().getView().getLayoutParams().height);\n            params.gravity = gravity;\n            getSnackbar().getView().setLayoutParams(params);\n        }\n        return this;\n    }\n\n    /**\n     * 设置Snackbar显示的位置,当Snackbar和CoordinatorLayout组合使用的时候\n     *\n     * @param gravity\n     */\n    public SnackbarUtil gravityCoordinatorLayout(int gravity) {\n        if (getSnackbar() != null) {\n            CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams(getSnackbar().getView().getLayoutParams().width, getSnackbar().getView().getLayoutParams().height);\n            params.gravity = gravity;\n            getSnackbar().getView().setLayoutParams(params);\n        }\n        return this;\n    }\n\n    /**\n     * 设置按钮文字内容 及 点击监听\n     * {@link Snackbar#setAction(CharSequence, View.OnClickListener)}\n     *\n     * @param resId\n     * @param listener\n     * @return\n     */\n    public SnackbarUtil setAction(@StringRes int resId, View.OnClickListener listener) {\n        if (getSnackbar() != null) {\n            return setAction(getSnackbar().getView().getResources().getText(resId), listener);\n        } else {\n            return this;\n        }\n    }\n\n    /**\n     * 设置按钮文字内容 及 点击监听\n     * {@link Snackbar#setAction(CharSequence, View.OnClickListener)}\n     *\n     * @param text\n     * @param listener\n     * @return\n     */\n    public SnackbarUtil setAction(CharSequence text, View.OnClickListener listener) {\n        if (getSnackbar() != null) {\n            getSnackbar().setAction(text, listener);\n        }\n        return this;\n    }\n\n    /**\n     * 设置 mSnackbar 展示完成 及 隐藏完成 的监听\n     *\n     * @param setCallback\n     * @return\n     */\n    public SnackbarUtil setCallback(Snackbar.Callback setCallback) {\n        if (getSnackbar() != null) {\n            getSnackbar().setCallback(setCallback);\n        }\n        return this;\n    }\n\n    /**\n     * 设置TextView(@+id/snackbar_text)左右两侧的图片\n     *\n     * @param leftDrawable\n     * @param rightDrawable\n     * @return\n     */\n    public SnackbarUtil leftAndRightDrawable(@Nullable @DrawableRes Integer leftDrawable, @Nullable @DrawableRes Integer rightDrawable) {\n        if (getSnackbar() != null) {\n            Drawable drawableLeft = null;\n            Drawable drawableRight = null;\n            if (leftDrawable != null) {\n                try {\n                    drawableLeft = getSnackbar().getView().getResources().getDrawable(leftDrawable.intValue());\n                } catch (Exception e) {\n                }\n            }\n            if (rightDrawable != null) {\n                try {\n                    drawableRight = getSnackbar().getView().getResources().getDrawable(rightDrawable.intValue());\n                } catch (Exception e) {\n                }\n            }\n            return leftAndRightDrawable(drawableLeft, drawableRight);\n        } else {\n            return this;\n        }\n    }\n\n    /**\n     * 设置TextView(@+id/snackbar_text)左右两侧的图片\n     *\n     * @param leftDrawable\n     * @param rightDrawable\n     * @return\n     */\n    public SnackbarUtil leftAndRightDrawable(@Nullable Drawable leftDrawable, @Nullable Drawable rightDrawable) {\n        if (getSnackbar() != null) {\n            TextView message = getSnackbar().getView().findViewById(R.id.snackbar_text);\n            LinearLayout.LayoutParams paramsMessage = (LinearLayout.LayoutParams) message.getLayoutParams();\n            paramsMessage = new LinearLayout.LayoutParams(paramsMessage.width, paramsMessage.height, 0.0f);\n            message.setLayoutParams(paramsMessage);\n            message.setCompoundDrawablePadding(message.getPaddingLeft());\n            int textSize = (int) message.getTextSize();\n            Log.e(\"Jet\", \"textSize:\" + textSize);\n            if (leftDrawable != null) {\n                leftDrawable.setBounds(0, 0, textSize, textSize);\n            }\n            if (rightDrawable != null) {\n                rightDrawable.setBounds(0, 0, textSize, textSize);\n            }\n            message.setCompoundDrawables(leftDrawable, null, rightDrawable, null);\n            LinearLayout.LayoutParams paramsSpace = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT, 1.0f);\n            ((Snackbar.SnackbarLayout) getSnackbar().getView()).addView(new Space(getSnackbar().getView().getContext()), 1, paramsSpace);\n        }\n        return this;\n    }\n\n    /**\n     * 设置TextView(@+id/snackbar_text)中文字的对齐方式 居中\n     *\n     * @return\n     */\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)\n    public SnackbarUtil messageCenter() {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {\n            if (getSnackbar() != null) {\n                TextView message = getSnackbar().getView().findViewById(R.id.snackbar_text);\n                //View.setTextAlignment需要SDK>=17\n                message.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY);\n                message.setGravity(Gravity.CENTER);\n            }\n        }\n        return this;\n    }\n\n    /**\n     * 设置TextView(@+id/snackbar_text)中文字的对齐方式 居右\n     *\n     * @return\n     */\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)\n    public SnackbarUtil messageRight() {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {\n            if (getSnackbar() != null) {\n                TextView message = getSnackbar().getView().findViewById(R.id.snackbar_text);\n                //View.setTextAlignment需要SDK>=17\n                message.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY);\n                message.setGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);\n            }\n        }\n        return this;\n    }\n\n    /**\n     * 向Snackbar布局中添加View(Google不建议,复杂的布局应该使用DialogFragment进行展示)\n     *\n     * @param layoutId 要添加的View的布局文件ID\n     * @param index\n     * @return\n     */\n    public SnackbarUtil addView(int layoutId, int index) {\n        if (getSnackbar() != null) {\n            //加载布局文件新建View\n            View addView = LayoutInflater.from(getSnackbar().getView().getContext()).inflate(layoutId, null);\n            return addView(addView, index);\n        } else {\n            return this;\n        }\n    }\n\n    /**\n     * 向Snackbar布局中添加View(Google不建议,复杂的布局应该使用DialogFragment进行展示)\n     *\n     * @param addView\n     * @param index\n     * @return\n     */\n    public SnackbarUtil addView(View addView, int index) {\n        if (getSnackbar() != null) {\n            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);//设置新建布局参数\n            //设置新建View在Snackbar内垂直居中显示\n            params.gravity = Gravity.CENTER_VERTICAL;\n            addView.setLayoutParams(params);\n            ((Snackbar.SnackbarLayout) getSnackbar().getView()).addView(addView, index);\n        }\n        return this;\n    }\n\n    /**\n     * 设置Snackbar布局的外边距\n     * 注:经试验发现,调用margins后再调用 gravityFrameLayout,则margins无效.\n     * 为保证margins有效,应该先调用 gravityFrameLayout,在 show() 之前调用 margins\n     *\n     * @param margin\n     * @return\n     */\n    public SnackbarUtil margins(int margin) {\n        if (getSnackbar() != null) {\n            return margins(margin, margin, margin, margin);\n        } else {\n            return this;\n        }\n    }\n\n    /**\n     * 设置Snackbar布局的外边距\n     * 注:经试验发现,调用margins后再调用 gravityFrameLayout,则margins无效.\n     * 为保证margins有效,应该先调用 gravityFrameLayout,在 show() 之前调用 margins\n     *\n     * @param left\n     * @param top\n     * @param right\n     * @param bottom\n     * @return\n     */\n    public SnackbarUtil margins(int left, int top, int right, int bottom) {\n        if (getSnackbar() != null) {\n            ViewGroup.LayoutParams params = getSnackbar().getView().getLayoutParams();\n            ((ViewGroup.MarginLayoutParams) params).setMargins(left, top, right, bottom);\n            getSnackbar().getView().setLayoutParams(params);\n        }\n        return this;\n    }\n\n    /**\n     * 经试验发现:\n     *      执行过{@link SnackbarUtil#backColor(int)}后:background instanceof ColorDrawable\n     *      未执行过{@link SnackbarUtil#backColor(int)}:background instanceof GradientDrawable\n     * @return\n     */\n    /*\n    public SnackbarUtil radius(){\n        Drawable background = snackbarWeakReference.get().getView().getBackground();\n        if(background instanceof GradientDrawable){\n            Log.e(\"Jet\",\"radius():GradientDrawable\");\n        }\n        if(background instanceof ColorDrawable){\n            Log.e(\"Jet\",\"radius():ColorDrawable\");\n        }\n        if(background instanceof StateListDrawable){\n            Log.e(\"Jet\",\"radius():StateListDrawable\");\n        }\n        Log.e(\"Jet\",\"radius()background:\"+background.getClass().getSimpleName());\n        return new SnackbarUtil(mSnackbar);\n    }\n    */\n\n    /**\n     * 通过SnackBar现在的背景,获取其设置圆角值时候所需的GradientDrawable实例\n     *\n     * @param backgroundOri\n     * @return\n     */\n    private GradientDrawable getRadiusDrawable(Drawable backgroundOri) {\n        GradientDrawable background = null;\n        if (backgroundOri instanceof GradientDrawable) {\n            background = (GradientDrawable) backgroundOri;\n        } else if (backgroundOri instanceof ColorDrawable) {\n            int backgroundColor = ((ColorDrawable) backgroundOri).getColor();\n            background = new GradientDrawable();\n            background.setColor(backgroundColor);\n        } else {\n        }\n        return background;\n    }\n\n    /**\n     * 设置Snackbar布局的圆角半径值\n     *\n     * @param radius 圆角半径\n     * @return\n     */\n    public SnackbarUtil radius(float radius) {\n        if (getSnackbar() != null) {\n            //将要设置给mSnackbar的背景\n            GradientDrawable background = getRadiusDrawable(getSnackbar().getView().getBackground());\n            if (background != null) {\n                radius = radius <= 0 ? 12 : radius;\n                background.setCornerRadius(radius);\n                getSnackbar().getView().setBackgroundDrawable(background);\n            }\n        }\n        return this;\n    }\n\n    /**\n     * 设置Snackbar布局的圆角半径值及边框颜色及边框宽度\n     *\n     * @param radius\n     * @param strokeWidth\n     * @param strokeColor\n     * @return\n     */\n    public SnackbarUtil radius(int radius, int strokeWidth, @ColorInt int strokeColor) {\n        if (getSnackbar() != null) {\n            //将要设置给mSnackbar的背景\n            GradientDrawable background = getRadiusDrawable(getSnackbar().getView().getBackground());\n            if (background != null) {\n                radius = radius <= 0 ? 12 : radius;\n                strokeWidth = strokeWidth <= 0 ? 1 : (strokeWidth >= getSnackbar().getView().findViewById(R.id.snackbar_text).getPaddingTop() ? 2 : strokeWidth);\n                background.setCornerRadius(radius);\n                background.setStroke(strokeWidth, strokeColor);\n                getSnackbar().getView().setBackgroundDrawable(background);\n            }\n        }\n        return this;\n    }\n\n    /**\n     * 计算单行的Snackbar的高度值(单位 pix)\n     *\n     * @return\n     */\n    private int calculateSnackBarHeight() {\n        /*\n        <TextView\n                android:id=\"@+id/snackbar_text\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:paddingTop=\"@dimen/design_snackbar_padding_vertical\"\n                android:paddingBottom=\"@dimen/design_snackbar_padding_vertical\"\n                android:paddingLeft=\"@dimen/design_snackbar_padding_horizontal\"\n                android:paddingRight=\"@dimen/design_snackbar_padding_horizontal\"\n                android:textAppearance=\"@style/TextAppearance.Design.Snackbar.Message\"\n                android:maxLines=\"@integer/design_snackbar_text_max_lines\"\n                android:layout_gravity=\"center_vertical|left|start\"\n                android:ellipsize=\"end\"\n                android:textAlignment=\"viewStart\"/>\n        */\n        //文字高度+paddingTop+paddingBottom : 14sp + 14dp*2\n        int SnackbarHeight = ScreenUtil.dp2px(getSnackbar().getView().getContext(), 28) + ScreenUtil.sp2px(getSnackbar().getView().getContext(), 14);\n        Log.e(\"Jet\", \"直接获取MessageView高度:\" + getSnackbar().getView().findViewById(R.id.snackbar_text).getHeight());\n        return SnackbarHeight;\n    }\n\n    /**\n     * 设置Snackbar显示在指定View的上方\n     * 注:暂时仅支持单行的Snackbar,因为{@link SnackbarUtil#calculateSnackBarHeight()}暂时仅支持单行Snackbar的高度计算\n     *\n     * @param targetView     指定View\n     * @param contentViewTop Activity中的View布局区域 距离屏幕顶端的距离\n     * @param marginLeft     左边距\n     * @param marginRight    右边距\n     * @return\n     */\n    public SnackbarUtil above(View targetView, int contentViewTop, int marginLeft, int marginRight) {\n        if (getSnackbar() != null) {\n            marginLeft = marginLeft <= 0 ? 0 : marginLeft;\n            marginRight = marginRight <= 0 ? 0 : marginRight;\n            int[] locations = new int[2];\n            targetView.getLocationOnScreen(locations);\n            Log.e(\"Jet\", \"距离屏幕左侧:\" + locations[0] + \"==距离屏幕顶部:\" + locations[1]);\n            int snackbarHeight = calculateSnackBarHeight();\n            Log.e(\"Jet\", \"Snackbar高度:\" + snackbarHeight);\n            //必须保证指定View的顶部可见 且 单行Snackbar可以完整的展示\n            if (locations[1] >= contentViewTop + snackbarHeight) {\n                gravityFrameLayout(Gravity.BOTTOM);\n                ViewGroup.LayoutParams params = getSnackbar().getView().getLayoutParams();\n                ((ViewGroup.MarginLayoutParams) params).setMargins(marginLeft, 0, marginRight, getSnackbar().getView().getResources().getDisplayMetrics().heightPixels - locations[1]);\n                getSnackbar().getView().setLayoutParams(params);\n            }\n        }\n        return this;\n    }\n\n    //CoordinatorLayout\n    public SnackbarUtil aboveCoordinatorLayout(View targetView, int contentViewTop, int marginLeft, int marginRight) {\n        if (getSnackbar() != null) {\n            marginLeft = marginLeft <= 0 ? 0 : marginLeft;\n            marginRight = marginRight <= 0 ? 0 : marginRight;\n            int[] locations = new int[2];\n            targetView.getLocationOnScreen(locations);\n            Log.e(\"Jet\", \"距离屏幕左侧:\" + locations[0] + \"==距离屏幕顶部:\" + locations[1]);\n            int snackbarHeight = calculateSnackBarHeight();\n            Log.e(\"Jet\", \"Snackbar高度:\" + snackbarHeight);\n            //必须保证指定View的顶部可见 且 单行Snackbar可以完整的展示\n            if (locations[1] >= contentViewTop + snackbarHeight) {\n                gravityCoordinatorLayout(Gravity.BOTTOM);\n                ViewGroup.LayoutParams params = getSnackbar().getView().getLayoutParams();\n                ((ViewGroup.MarginLayoutParams) params).setMargins(marginLeft, 0, marginRight, getSnackbar().getView().getResources().getDisplayMetrics().heightPixels - locations[1]);\n                getSnackbar().getView().setLayoutParams(params);\n            }\n        }\n        return this;\n    }\n\n    /**\n     * 设置Snackbar显示在指定View的下方\n     * 注:暂时仅支持单行的Snackbar,因为{@link SnackbarUtil#calculateSnackBarHeight()}暂时仅支持单行Snackbar的高度计算\n     *\n     * @param targetView     指定View\n     * @param contentViewTop Activity中的View布局区域 距离屏幕顶端的距离\n     * @param marginLeft     左边距\n     * @param marginRight    右边距\n     * @return\n     */\n    public SnackbarUtil bellow(View targetView, int contentViewTop, int marginLeft, int marginRight) {\n        if (getSnackbar() != null) {\n            marginLeft = marginLeft <= 0 ? 0 : marginLeft;\n            marginRight = marginRight <= 0 ? 0 : marginRight;\n            int[] locations = new int[2];\n            targetView.getLocationOnScreen(locations);\n            int snackbarHeight = calculateSnackBarHeight();\n            int screenHeight = ScreenUtil.getScreenHeight(getSnackbar().getView().getContext());\n            //必须保证指定View的底部可见 且 单行Snackbar可以完整的展示\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n                //为什么要'+2'? 因为在Android L(Build.VERSION_CODES.LOLLIPOP)以上,例如Button会有一定的'阴影(shadow)',阴影的大小由'高度(elevation)'决定.\n                //为了在Android L以上的系统中展示的Snackbar不要覆盖targetView的阴影部分太大比例,所以人为减小2px的layout_marginBottom属性.\n                if (locations[1] + targetView.getHeight() >= contentViewTop && locations[1] + targetView.getHeight() + snackbarHeight + 2 <= screenHeight) {\n                    gravityFrameLayout(Gravity.BOTTOM);\n                    ViewGroup.LayoutParams params = getSnackbar().getView().getLayoutParams();\n                    ((ViewGroup.MarginLayoutParams) params).setMargins(marginLeft, 0, marginRight, screenHeight - (locations[1] + targetView.getHeight() + snackbarHeight + 2));\n                    getSnackbar().getView().setLayoutParams(params);\n                }\n            } else {\n                if (locations[1] + targetView.getHeight() >= contentViewTop && locations[1] + targetView.getHeight() + snackbarHeight <= screenHeight) {\n                    gravityFrameLayout(Gravity.BOTTOM);\n                    ViewGroup.LayoutParams params = getSnackbar().getView().getLayoutParams();\n                    ((ViewGroup.MarginLayoutParams) params).setMargins(marginLeft, 0, marginRight, screenHeight - (locations[1] + targetView.getHeight() + snackbarHeight));\n                    getSnackbar().getView().setLayoutParams(params);\n                }\n            }\n        }\n        return this;\n    }\n\n    public SnackbarUtil bellowCoordinatorLayout(View targetView, int contentViewTop, int marginLeft, int marginRight) {\n        if (getSnackbar() != null) {\n            marginLeft = marginLeft <= 0 ? 0 : marginLeft;\n            marginRight = marginRight <= 0 ? 0 : marginRight;\n            int[] locations = new int[2];\n            targetView.getLocationOnScreen(locations);\n            int snackbarHeight = calculateSnackBarHeight();\n            int screenHeight = ScreenUtil.getScreenHeight(getSnackbar().getView().getContext());\n            //必须保证指定View的底部可见 且 单行Snackbar可以完整的展示\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n                //为什么要'+2'? 因为在Android L(Build.VERSION_CODES.LOLLIPOP)以上,例如Button会有一定的'阴影(shadow)',阴影的大小由'高度(elevation)'决定.\n                //为了在Android L以上的系统中展示的Snackbar不要覆盖targetView的阴影部分太大比例,所以人为减小2px的layout_marginBottom属性.\n                if (locations[1] + targetView.getHeight() >= contentViewTop && locations[1] + targetView.getHeight() + snackbarHeight + 2 <= screenHeight) {\n                    gravityCoordinatorLayout(Gravity.BOTTOM);\n                    ViewGroup.LayoutParams params = getSnackbar().getView().getLayoutParams();\n                    ((ViewGroup.MarginLayoutParams) params).setMargins(marginLeft, 0, marginRight, screenHeight - (locations[1] + targetView.getHeight() + snackbarHeight + 2));\n                    getSnackbar().getView().setLayoutParams(params);\n                }\n            } else {\n                if (locations[1] + targetView.getHeight() >= contentViewTop && locations[1] + targetView.getHeight() + snackbarHeight <= screenHeight) {\n                    gravityCoordinatorLayout(Gravity.BOTTOM);\n                    ViewGroup.LayoutParams params = getSnackbar().getView().getLayoutParams();\n                    ((ViewGroup.MarginLayoutParams) params).setMargins(marginLeft, 0, marginRight, screenHeight - (locations[1] + targetView.getHeight() + snackbarHeight));\n                    getSnackbar().getView().setLayoutParams(params);\n                }\n            }\n        }\n        return this;\n    }\n\n\n    /**\n     * 显示 mSnackbar\n     */\n    public void show() {\n        Log.e(\"Jet\", \"show()\");\n        if (getSnackbar() != null) {\n            Log.e(\"Jet\", \"show\");\n            getSnackbar().show();\n        } else {\n            Log.e(\"Jet\", \"已经被回收\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/StringJoiner.java",
    "content": "package me.wizos.loread.utils;\n\n\nimport androidx.annotation.NonNull;\n\nimport java.util.Objects;\n\npublic class StringJoiner {\n    private String emptyValue;\n    // 前缀\n    private final String prefix;\n    // 分隔符\n    private final String delimiter;\n    // 后缀\n    private final String suffix;\n    // 值\n    private StringBuilder value;\n\n    /**\n     * 构造器\n     */\n    public StringJoiner(CharSequence delimiter) {\n        this(delimiter, \"\", \"\");\n    }\n\n    public StringJoiner(CharSequence delimiter,\n                        CharSequence prefix,\n                        CharSequence suffix) {\n        Objects.requireNonNull(prefix, \"The prefix must not be null\");\n        Objects.requireNonNull(delimiter, \"The delimiter must not be null\");\n        Objects.requireNonNull(suffix, \"The suffix must not be null\");\n        // make defensive copies of arguments\n        this.prefix = prefix.toString();\n        this.delimiter = delimiter.toString();\n        this.suffix = suffix.toString();\n        this.emptyValue = this.prefix + this.suffix;\n    }\n\n    // 拼接\n    public StringJoiner add(CharSequence newElement) {\n        prepareBuilder().append(newElement);\n        return this;\n    }\n\n    // 预拼接value\n    private StringBuilder prepareBuilder() {\n        // value已加前缀\n        if (value != null) {\n            // 此时添加分隔符\n            value.append(delimiter);\n        } else {\n            // value未加前缀时需要先添加前缀\n            value = new StringBuilder().append(prefix);\n        }\n        return value;\n    }\n\n    //重写了toString 方法\n    @NonNull\n    @Override\n    public String toString() {\n        if (value == null) {\n            // value未进行任何字符拼接时反悔emptyValue\n            return emptyValue;\n        } else {\n            // 后缀为\"\"字符时，直接返回value\n            if (suffix.equals(\"\")) {\n                return value.toString();\n            } else {\n                // 获取value未拼接后缀的长度\n                int initialLength = value.length();\n                String result = value.append(suffix).toString();\n                // reset value to pre-append initialLength\n                // 此处是为了保证value.toString()为未拼接后缀前的字符串\n                value.setLength(initialLength);\n                return result;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/StringUtils.java",
    "content": "package me.wizos.loread.utils;\n\nimport android.text.TextUtils;\nimport android.util.Base64;\n\nimport androidx.annotation.StringRes;\n\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLEncoder;\nimport java.nio.charset.StandardCharsets;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport me.wizos.loread.App;\n\n\n@SuppressWarnings({\"unused\", \"WeakerAccess\"})\npublic class StringUtils {\n    private static final String TAG = \"StringUtils\";\n    private final static HashMap<Character, Integer> ChnMap = getChnMap();\n\n    public static boolean isEmpty(List list) {\n        return list == null || list.isEmpty() || list.size() == 0;\n    }\n    public static boolean isEmpty(CharSequence str) {\n        return str == null || str.length() == 0;\n    }\n\n//    public static String readable(String source, String pattern) {\n//        @SuppressLint(\"SimpleDateFormat\") DateFormat format = new SimpleDateFormat(pattern);\n//        Calendar calendar = Calendar.getInstance();\n//        try {\n//            Date date = format.parse(source);\n//            long curTime = calendar.getTimeInMillis();\n//            calendar.setTime(date);\n//            //将MISC 转换成 sec\n//            long difSec = Math.abs((curTime - date.getTime()) / 1000);\n//            long difMin = difSec / 60;\n//            long difHour = difMin / 60;\n//            long difDate = difHour / 60;\n//            int oldHour = calendar.get(Calendar.HOUR);\n//            //如果没有时间\n//            if (oldHour == 0) {\n//                //比日期:昨天今天和明天\n//                if (difDate == 0) {\n//                    return \"今天\";\n//                } else if (difDate < DAY_OF_YESTERDAY) {\n//                    return \"昨天\";\n//                } else {\n//                    @SuppressLint(\"SimpleDateFormat\") DateFormat convertFormat = new SimpleDateFormat(\"yyyy-MM-dd\");\n//                    return convertFormat.format(date);\n//                }\n//            }\n//\n//            if (difSec < TIME_UNIT) {\n//                return difSec + \"秒前\";\n//            } else if (difMin < TIME_UNIT) {\n//                return difMin + \"分钟前\";\n//            } else if (difHour < HOUR_OF_DAY) {\n//                return difHour + \"小时前\";\n//            } else if (difDate < DAY_OF_YESTERDAY) {\n//                return \"昨天\";\n//            } else {\n//                @SuppressLint(\"SimpleDateFormat\") DateFormat convertFormat = new SimpleDateFormat(\"yyyy-MM-dd\");\n//                return convertFormat.format(date);\n//            }\n//        } catch (ParseException e) {\n//            e.printStackTrace();\n//        }\n//        return \"\";\n//    }\n\n    public static String toFirstCapital(String str) {\n        return str.substring(0, 1).toUpperCase() + str.substring(1);\n    }\n\n    public static String getString(@StringRes int id) {\n        return App.i().getResources().getString(id);\n    }\n\n    public static String getString(@StringRes int id, Object... formatArgs) {\n        return App.i().getString(id, formatArgs);\n    }\n\n    /**\n     * 只对url中的汉字部分进行 urlEncode 。\n     * @param url\n     * @return\n     */\n    public static String urlEncode(String url){\n        if( TextUtils.isEmpty(url)){\n            return null;\n        }\n        try {\n            StringBuffer sb = new StringBuffer();\n            for (int i = 0, length = url.length(); i < length; i++) {\n                char c = url.charAt(i);\n                if (c <= '\\u001f' || c >= '\\u007f') {\n                    sb.append( URLEncoder.encode( String.valueOf(c),\"utf-8\") );\n                } else {\n                    sb.append(c);\n                }\n            }\n            return sb.toString();\n        }catch (UnsupportedEncodingException e){\n            e.printStackTrace();\n            return url;\n        }\n    }\n\n    /**\n     * 将文本中的半角字符，转换成全角字符\n     */\n    public static String halfToFull(String input) {\n        char[] c = input.toCharArray();\n        for (int i = 0; i < c.length; i++) {\n            if (c[i] == 32) //半角空格\n            {\n                c[i] = (char) 12288;\n                continue;\n            }\n            //根据实际情况，过滤不需要转换的符号\n            //if (c[i] == 46) //半角点号，不转换\n            // continue;\n\n            if (c[i] > 32 && c[i] < 127)    //其他符号都转换为全角\n                c[i] = (char) (c[i] + 65248);\n        }\n        return new String(c);\n    }\n\n    //功能：字符串全角转换为半角\n    public static String fullToHalf(String input) {\n        char[] c = input.toCharArray();\n        for (int i = 0; i < c.length; i++) {\n            if (c[i] == 12288) //全角空格\n            {\n                c[i] = (char) 32;\n                continue;\n            }\n\n            if (c[i] > 65280 && c[i] < 65375) {\n                c[i] = (char) (c[i] - 65248);\n            }\n\n        }\n        return new String(c);\n    }\n\n    private static HashMap<Character, Integer> getChnMap() {\n        HashMap<Character, Integer> map = new HashMap<>();\n        String cnStr = \"零一二三四五六七八九十\";\n        char[] c = cnStr.toCharArray();\n        for (int i = 0; i <= 10; i++) {\n            map.put(c[i], i);\n        }\n        cnStr = \"〇壹贰叁肆伍陆柒捌玖拾\";\n        c = cnStr.toCharArray();\n        for (int i = 0; i <= 10; i++) {\n            map.put(c[i], i);\n        }\n        map.put('两', 2);\n        map.put('百', 100);\n        map.put('佰', 100);\n        map.put('千', 1000);\n        map.put('仟', 1000);\n        map.put('万', 10000);\n        map.put('亿', 100000000);\n        return map;\n    }\n\n    @SuppressWarnings(\"ConstantConditions\")\n    public static int chineseNumToInt(String chNum) {\n        int result = 0;\n        int tmp = 0;\n        int billion = 0;\n        char[] cn = chNum.toCharArray();\n\n        // \"一零二五\" 形式\n        if (cn.length > 1 && chNum.matches(\"^[〇零一二三四五六七八九壹贰叁肆伍陆柒捌玖]$\")) {\n            for (int i = 0; i < cn.length; i++) {\n                cn[i] = (char) (48 + ChnMap.get(cn[i]));\n            }\n            return Integer.parseInt(new String(cn));\n        }\n\n        // \"一千零二十五\", \"一千二\" 形式\n        try {\n            for (int i = 0; i < cn.length; i++) {\n                int tmpNum = ChnMap.get(cn[i]);\n                if (tmpNum == 100000000) {\n                    result += tmp;\n                    result *= tmpNum;\n                    billion = billion * 100000000 + result;\n                    result = 0;\n                    tmp = 0;\n                } else if (tmpNum == 10000) {\n                    result += tmp;\n                    result *= tmpNum;\n                    tmp = 0;\n                } else if (tmpNum >= 10) {\n                    if (tmp == 0)\n                        tmp = 1;\n                    result += tmpNum * tmp;\n                    tmp = 0;\n                } else {\n                    if (i >= 2 && i == cn.length - 1 && ChnMap.get(cn[i - 1]) > 10)\n                        tmp = tmpNum * ChnMap.get(cn[i - 1]) / 10;\n                    else\n                        tmp = tmp * 10 + tmpNum;\n                }\n            }\n            result += tmp + billion;\n            return result;\n        } catch (Exception e) {\n            return -1;\n        }\n    }\n\n    public static int stringToInt(String str) {\n        if (str != null) {\n            String num = fullToHalf(str).replaceAll(\"\\\\s\", \"\");\n            try {\n                return Integer.parseInt(num);\n            } catch (Exception e) {\n                return chineseNumToInt(num);\n            }\n        }\n        return -1;\n    }\n\n    public static String base64Decode(String str) {\n        byte[] bytes = Base64.decode(str, Base64.DEFAULT);\n        try {\n            return new String(bytes, StandardCharsets.UTF_8);\n        } catch (Exception e) {\n            return new String(bytes);\n        }\n    }\n\n    public static String escape(String src) {\n        int i;\n        char j;\n        StringBuilder tmp = new StringBuilder();\n        tmp.ensureCapacity(src.length() * 6);\n        for (i = 0; i < src.length(); i++) {\n            j = src.charAt(i);\n            if (Character.isDigit(j) || Character.isLowerCase(j)\n                    || Character.isUpperCase(j))\n                tmp.append(j);\n            else if (j < 256) {\n                tmp.append(\"%\");\n                if (j < 16)\n                    tmp.append(\"0\");\n                tmp.append(Integer.toString(j, 16));\n            } else {\n                tmp.append(\"%u\");\n                tmp.append(Integer.toString(j, 16));\n            }\n        }\n        return tmp.toString();\n    }\n\n    public static boolean isJsonType(String str) {\n        boolean result = false;\n        if (!TextUtils.isEmpty(str)) {\n            str = str.trim();\n            if (str.startsWith(\"{\") && str.endsWith(\"}\")) {\n                result = true;\n            } else if (str.startsWith(\"[\") && str.endsWith(\"]\")) {\n                result = true;\n            }\n        }\n        return result;\n    }\n\n    public static boolean isJsonObject(String text) {\n        boolean result = false;\n        if (!TextUtils.isEmpty(text)) {\n            text = text.trim();\n            if (text.startsWith(\"{\") && text.endsWith(\"}\")) {\n                result = true;\n            }\n        }\n        return result;\n    }\n\n    public static boolean isJsonArray(String text) {\n        boolean result = false;\n        if (!TextUtils.isEmpty(text)) {\n            text = text.trim();\n            if (text.startsWith(\"[\") && text.endsWith(\"]\")) {\n                result = true;\n            }\n        }\n        return result;\n    }\n\n    public static boolean isTrimEmpty(String text) {\n        if (text == null) return true;\n        if (text.length() == 0) return true;\n        return text.trim().length() == 0;\n    }\n\n    public static boolean startWithIgnoreCase(String src, String obj) {\n        if (src == null || obj == null) return false;\n        if (obj.length() > src.length()) return false;\n        return src.substring(0, obj.length()).equalsIgnoreCase(obj);\n    }\n\n    public static boolean endWithIgnoreCase(String src, String obj) {\n        if (src == null || obj == null) return false;\n        if (obj.length() > src.length()) return false;\n        return src.substring(src.length() - obj.length()).equalsIgnoreCase(obj);\n    }\n\n    /**\n     * delimiter 分隔符\n     * elements 需要连接的字符数组\n     */\n    public static String join(CharSequence delimiter, CharSequence... elements) {\n        // 空指针判断\n        Objects.requireNonNull(delimiter);\n        Objects.requireNonNull(elements);\n\n        // Number of elements not likely worth Arrays.stream overhead.\n        // 此处用到了StringJoiner(JDK 8引入的类）\n        // 先构造一个以参数delimiter为分隔符的StringJoiner对象\n        StringJoiner joiner = new StringJoiner(delimiter);\n        for (CharSequence cs : elements) {\n            // 拼接字符\n            joiner.add(cs);\n        }\n        return joiner.toString();\n    }\n\n    public static String join(CharSequence delimiter, Iterable<? extends CharSequence> elements) {\n        if (elements == null) return null;\n        if (delimiter == null) delimiter = \",\";\n        StringJoiner joiner = new StringJoiner(delimiter);\n        for (CharSequence cs : elements) {\n            joiner.add(cs);\n        }\n        return joiner.toString();\n    }\n\n    public static boolean isContainNumber(String company) {\n        Pattern p = Pattern.compile(\"[0-9]\");\n        Matcher m = p.matcher(company);\n        return m.find();\n    }\n\n    public static boolean isNumeric(String str) {\n        Pattern pattern = Pattern.compile(\"[0-9]*\");\n        Matcher isNum = pattern.matcher(str);\n        return isNum.matches();\n    }\n\n    public static String getBaseUrl(String url) {\n        if (url == null || !url.startsWith(\"http\")) return null;\n        int index = url.indexOf(\"/\", 9);\n        if (index == -1) {\n            return url;\n        }\n        return url.substring(0, index);\n    }\n\n    // 移除字符串首尾空字符的高效方法(利用ASCII值判断,包括全角空格)\n    public static String trim(String s) {\n        if (isEmpty(s)) return \"\";\n        int start = 0, len = s.length();\n        int end = len - 1;\n        while ((start < end) && ((s.charAt(start) <= 0x20) || (s.charAt(start) == '　'))) {\n            ++start;\n        }\n        while ((start < end) && ((s.charAt(end) <= 0x20) || (s.charAt(end) == '　'))) {\n            --end;\n        }\n        if (end < len) ++end;\n        return ((start > 0) || (end < len)) ? s.substring(start, end) : s;\n    }\n\n    public static String repeat(String str, int n) {\n        StringBuilder stringBuilder = new StringBuilder();\n        for (int i = 0; i < n; i++) {\n            stringBuilder.append(str);\n        }\n        return stringBuilder.toString();\n    }\n\n    public static String removeUTFCharacters(String data) {\n        if (data == null) return null;\n        Pattern p = Pattern.compile(\"\\\\\\\\u(\\\\p{XDigit}{4})\");\n        Matcher m = p.matcher(data);\n        StringBuffer buf = new StringBuffer(data.length());\n        while (m.find()) {\n            String ch = String.valueOf((char) Integer.parseInt(m.group(1), 16));\n            m.appendReplacement(buf, Matcher.quoteReplacement(ch));\n        }\n        m.appendTail(buf);\n        return buf.toString();\n    }\n\n    public static String formatHtml(String html) {\n        if (isEmpty(html)) return \"\";\n        return html.replaceAll(\"(?i)<(br[\\\\s/]*|/*p.*?|/*div.*?)>\", \"\\n\")// 替换特定标签为换行符\n                .replaceAll(\"<[script>]*.*?>|&nbsp;\", \"\")// 删除script标签对和空格转义符\n                .replaceAll(\"\\\\s*\\\\n+\\\\s*\", \"\\n　　\")// 移除空行,并增加段前缩进2个汉字\n                .replaceAll(\"^[\\\\n\\\\s]+\", \"　　\")//移除开头空行,并增加段前缩进2个汉字\n                .replaceAll(\"[\\\\n\\\\s]+$\", \"\");//移除尾部空行\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/SymbolUtil.java",
    "content": "package me.wizos.loread.utils;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Created by zhuleiyue on 2017/3/20.\n * <p>\n * emoji regex\n */\n\nclass SymbolUtil {\n    private static final String MiscellaneousSymbolsAndPictographs = \"[\\\\uD83C\\\\uDF00-\\\\uD83D\\\\uDDFF]\";\n\n    private static final String SupplementalSymbolsAndPictographs = \"[\\\\uD83E\\\\uDD00-\\\\uD83E\\\\uDDFF]\";\n\n    private static final String Emoticons = \"[\\\\uD83D\\\\uDE00-\\\\uD83D\\\\uDE4F]\";\n\n    private static final String TransportAndMapSymbols = \"[\\\\uD83D\\\\uDE80-\\\\uD83D\\\\uDEFF]\";\n\n    private static final String MiscellaneousSymbols = \"[\\\\u2600-\\\\u26FF]\\\\uFE0F?\";\n\n    private static final String Dingbats = \"[\\\\u2700-\\\\u27BF]\\\\uFE0F?\";\n\n    private static final String EnclosedAlphanumerics = \"\\\\u24C2\\\\uFE0F?\";\n\n    private static final String RegionalIndicatorSymbol = \"[\\\\uD83C\\\\uDDE6-\\\\uD83C\\\\uDDFF]{1,2}\";\n\n    private static final String EnclosedAlphanumericSupplement = \"[\\\\uD83C\\\\uDD70\\\\uD83C\\\\uDD71\\\\uD83C\\\\uDD7E\\\\uD83C\\\\uDD7F\\\\uD83C\\\\uDD8E\\\\uD83C\\\\uDD91-\\\\uD83C\\\\uDD9A]\\\\uFE0F?\";\n\n    private static final String BasicLatin = \"[\\\\u0023\\\\u002A\\\\u0030-\\\\u0039]\\\\uFE0F?\\\\u20E3\";\n\n    private static final String Arrows = \"[\\\\u2194-\\\\u2199\\\\u21A9-\\\\u21AA]\\\\uFE0F?\";\n\n    private static final String MiscellaneousSymbolsAndArrows = \"[\\\\u2B05-\\\\u2B07\\\\u2B1B\\\\u2B1C\\\\u2B50\\\\u2B55]\\\\uFE0F?\";\n\n    private static final String SupplementalArrows = \"[\\\\u2934\\\\u2935]\\\\uFE0F?\";\n\n    private static final String CJKSymbolsAndPunctuation = \"[\\\\u3030\\\\u303D]\\\\uFE0F?\";\n\n    private static final String EnclosedCJKLettersAndMonths = \"[\\\\u3297\\\\u3299]\\\\uFE0F?\";\n\n    private static final String EnclosedIdeographicSupplement = \"[\\\\uD83C\\\\uDE01\\\\uD83C\\\\uDE02\\\\uD83C\\\\uDE1A\\\\uD83C\\\\uDE2F\\\\uD83C\\\\uDE32-\\\\uD83C\\\\uDE3A\\\\uD83C\\\\uDE50\\\\uD83C\\\\uDE51]\\\\uFE0F?\";\n\n    private static final String GeneralPunctuation = \"[\\\\u203C\\\\u2049]\\\\uFE0F?\";\n\n    private static final String GeometricShapes = \"[\\\\u25AA\\\\u25AB\\\\u25B6\\\\u25C0\\\\u25FB-\\\\u25FE]\\\\uFE0F?\";\n\n    private static final String LatinSupplement = \"[\\\\u00A9\\\\u00AE]\\\\uFE0F?\";\n\n    private static final String LetterlikeSymbols = \"[\\\\u2122\\\\u2139]\\\\uFE0F?\";\n\n    private static final String MahjongTiles = \"\\\\uD83C\\\\uDC04\\\\uFE0F?\";\n\n    private static final String PlayingCards = \"\\\\uD83C\\\\uDCCF\\\\uFE0F?\";\n\n    private static final String MiscellaneousTechnical = \"[\\\\u231A\\\\u231B\\\\u2328\\\\u23CF\\\\u23E9-\\\\u23F3\\\\u23F8-\\\\u23FA]\\\\uFE0F?\";\n\n    private static String getEmojiRegex() {\n        return \"(?:\"\n                + MiscellaneousSymbolsAndPictographs + \"|\"\n                + SupplementalSymbolsAndPictographs + \"|\"\n                + Emoticons + \"|\"\n                + TransportAndMapSymbols + \"|\"\n                + MiscellaneousSymbols + \"|\"\n                + Dingbats + \"|\"\n                + EnclosedAlphanumerics + \"|\"\n                + RegionalIndicatorSymbol + \"|\"\n                + EnclosedAlphanumericSupplement + \"|\"\n                + BasicLatin + \"|\"\n                + Arrows + \"|\"\n                + MiscellaneousSymbolsAndArrows + \"|\"\n                + SupplementalArrows + \"|\"\n                + CJKSymbolsAndPunctuation + \"|\"\n                + EnclosedCJKLettersAndMonths + \"|\"\n                + EnclosedIdeographicSupplement + \"|\"\n                + GeneralPunctuation + \"|\"\n                + GeometricShapes + \"|\"\n                + LatinSupplement + \"|\"\n                + LetterlikeSymbols + \"|\"\n                + MahjongTiles + \"|\"\n                + PlayingCards + \"|\"\n                + MiscellaneousTechnical + \")\";\n    }\n\n    static String filterEmoji(String source) {\n        if (source == null) {\n            source = \"\";\n        }\n        Pattern emoji = Pattern.compile(getEmojiRegex(), Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE);// 后2个参数要进行大小不明感的匹配\n        Matcher emojiMatcher = emoji.matcher(source);\n        if (emojiMatcher.find()) {\n            source = emojiMatcher.replaceAll(\"\");\n        }\n        return source;\n    }\n\n\n    /**\n     * 过滤保存文件到存储器中会有问题的符号\n     * @param str\n     * @return\n     */\n    static String filterUnsavedSymbol(String str) {\n        return str\n                .replace(\"\\\\\", \"\")\n                .replace(\"/\", \"\")\n                .replace(\":\", \"\")\n                .replace(\"*\", \"\")\n                .replace(\"?\", \"\")\n                .replace(\"\\\"\", \"\")\n                .replace(\"<\", \"\")\n                .replace(\">\", \"\")\n                .replace(\"|\", \"\")\n                .replace(\"%\", \"_\")\n                .replace(\"#\", \"_\")\n                .replace(\"&amp;\", \"_\")\n                .replace(\"&nbsp;\", \"_\")\n                .replace(\"&\", \"_\")\n                .replace(\"�\", \"_\")\n                .replace(\"\\r\", \"_\")\n                .replace(\"\\n\", \"_\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/TimeUtil.java",
    "content": "package me.wizos.loread.utils;\n\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.GregorianCalendar;\nimport java.util.Locale;\n\n/**\n * 关于时间操作的工具类\n * Created by xdsjs on 2015/10/14.\n */\npublic class TimeUtil {\n//    public static String formatReadability(long timestamp)  {\n//        // 如果给定的时间小于昨天凌晨，则直接用标准的 yyyy-MM-dd HH:mm 格式\n//    }\n\n    /**\n     * 时间差\n     *\n     * @param date\n     * @return\n     */\n    public static String getTimeFormatText(Date date) {\n        long minute = 60 * 1000;// 1分钟\n        long hour = 60 * minute;// 1小时\n        long day = 24 * hour;// 1天\n        long month = 31 * day;// 月\n        long year = 12 * month;// 年\n\n        if (date == null) {\n            return null;\n        }\n        long diff = new Date().getTime() - date.getTime();\n        long r = 0;\n        if (diff > year) {\n            r = (diff / year);\n            return r + \"年前\";\n        }\n        if (diff > month) {\n            r = (diff / month);\n            return r + \"个月前\";\n        }\n        if (diff > day) {\n            r = (diff / day);\n            return r + \"天前\";\n        }\n        if (diff > hour) {\n            r = (diff / hour);\n            return r + \"小时前\";\n        }\n        if (diff > minute) {\n            r = (diff / minute);\n            return r + \"分钟前\";\n        }\n        return \"刚刚\";\n    }\n\n\n    /** * 用于显示时间 */\n    public static final String TODAY = \"今天\";\n    public static final String YESTERDAY = \"昨天\";\n\n    public static String getToday(String time)  {\n        Calendar pre = Calendar.getInstance();\n        Date predate = new Date(System.currentTimeMillis());\n        pre.setTime(predate);\n\n        Calendar cal = Calendar.getInstance();\n        Date date = new Date(Long.parseLong(time) * 1000);\n        cal.setTime(date);\n\n        if (cal.get(Calendar.YEAR) == (pre.get(Calendar.YEAR))) {\n            int diffDay = cal.get(Calendar.DAY_OF_YEAR) - pre.get(Calendar.DAY_OF_YEAR);\n            return showDateDetail(diffDay, time);\n        }\n        return time;\n    }\n\n    /** * 将日期差显示为今天、明天或者星期 * @param diffDay * @param time * @return */\n    private static String showDateDetail(int diffDay, String time){\n        switch(diffDay){\n            case -1:\n                return YESTERDAY;\n            case 0:\n                return TODAY;\n            default:\n                return  getWeek(time);\n        }\n    }\n    /** * 计算周几 */\n    public static String getWeek(String data) {\n        SimpleDateFormat sdr = new SimpleDateFormat(\"yyyy年MM月dd日HH时mm分ss秒\");\n        long lcc = Long.valueOf(data);\n        int i = Integer.parseInt(data);\n        String times = sdr.format(new Date(i * 1000L));\n        Date date = null;\n        int mydate = 0;\n        String week = \"\";\n        try {\n            date = sdr.parse(times);\n            Calendar cd = Calendar.getInstance();\n            cd.setTime(date);\n            mydate = cd.get(Calendar.DAY_OF_WEEK);\n            // 获取指定日期转换成星期几\n        } catch (ParseException e) {\n            // TODO Auto-generated catch block\n            e.printStackTrace();\n        }\n        if (mydate == 1) {\n            week = \"星期日\";\n        } else if (mydate == 2) {\n            week = \"星期一\";\n        } else if (mydate == 3) {\n            week = \"星期二\";\n        } else if (mydate == 4) {\n            week = \"星期三\";\n        } else if (mydate == 5) {\n            week = \"星期四\";\n        } else if (mydate == 6) {\n            week = \"星期五\";\n        } else if (mydate == 7) {\n            week = \"星期六\";\n        }\n        return week;\n    }\n\n\n\n\n\n    /**\n     * 将 时间戳 转为指定的 格式\n     * @param timestamp 时间戳（毫秒）\n     * @param pattern 要转为的格式（例如 yyyy-MM-dd HH:mm:ss）\n     * @return 格式化的时间\n     */\n    public static String format(long timestamp, String pattern) {\n        SimpleDateFormat dateFormat = new SimpleDateFormat(pattern, Locale.getDefault());\n        Date date = new Date(timestamp);\n        return dateFormat.format(date);\n    }\n\n    public static int getCurrentHour() {\n        Calendar currentDate = new GregorianCalendar(Locale.CHINA);\n        return currentDate.get(Calendar.HOUR_OF_DAY);\n    }\n\n    public static StringBuilder getTime(int time) {\n        if (time < 0) {\n            time = 0;\n        }\n        int cache = time / 1000;\n        int second = cache % 60;\n        cache = cache / 60;\n        int minute = cache % 60;\n        int hour = cache / 60;\n        StringBuilder timeStamp = new StringBuilder();\n        if (hour > 0) {\n            timeStamp.append(hour);\n            timeStamp.append(\":\");\n        }\n        if (minute < 10) {\n            timeStamp.append(\"0\");\n        }\n        timeStamp.append(minute);\n        timeStamp.append(\":\");\n\n        if (second < 10) {\n            timeStamp.append(\"0\");\n        }\n        timeStamp.append(second);\n        return timeStamp;\n    }\n\n    /**\n     * 获取当天的开始时间\n     */\n    public static long getTimeOfDay() {\n        Calendar currentDate = new GregorianCalendar();\n        currentDate.set(Calendar.HOUR_OF_DAY, 0);\n        currentDate.set(Calendar.MINUTE, 0);\n        currentDate.set(Calendar.SECOND, 0);\n        return currentDate.getTime().getTime();\n    }\n\n    /**\n     * 获取当前周的第一天的开始时间\n     */\n    public static long getFirstDayTimeOfWeek() {\n        Calendar currentDate = new GregorianCalendar();\n        currentDate.setFirstDayOfWeek(Calendar.SUNDAY);\n        currentDate.set(Calendar.HOUR_OF_DAY, 0);\n        currentDate.set(Calendar.MINUTE, 0);\n        currentDate.set(Calendar.SECOND, 0);\n        currentDate.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);\n        return currentDate.getTime().getTime();\n    }\n\n    /**\n     * 获取当前月的第一天的开始时间\n     */\n    public static long getFirstDayTimeOfMonth() {\n        Calendar currentDate = new GregorianCalendar();\n        currentDate.setFirstDayOfWeek(Calendar.MONDAY);\n        currentDate.set(Calendar.HOUR_OF_DAY, 0);\n        currentDate.set(Calendar.MINUTE, 0);\n        currentDate.set(Calendar.SECOND, 0);\n        currentDate.set(Calendar.DAY_OF_MONTH, 0);\n        return currentDate.getTime().getTime();\n    }\n\n    /**\n     * 获取当前年的第一天的开始时间\n     */\n    public static long getFirstDayTimeOfYear() {\n        Calendar currentDate = new GregorianCalendar();\n        currentDate.setFirstDayOfWeek(Calendar.MONDAY);\n        currentDate.set(Calendar.HOUR_OF_DAY, 0);\n        currentDate.set(Calendar.MINUTE, 0);\n        currentDate.set(Calendar.SECOND, 0);\n        currentDate.set(Calendar.DAY_OF_YEAR, 0);\n        return currentDate.getTime().getTime();\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/Tool.java",
    "content": "package me.wizos.loread.utils;\n\nimport android.app.ActivityManager;\nimport android.content.ComponentName;\nimport android.content.Context;\nimport android.text.TextUtils;\nimport android.view.View;\n\nimport com.hjq.toast.ToastUtils;\nimport com.socks.library.KLog;\n\nimport java.text.DecimalFormat;\nimport java.util.List;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.BuildConfig;\nimport me.wizos.loread.R;\n\n/**\n * 一些比较杂的工具函数\n * Created by Wizos on 2016/11/1.\n */\n\npublic class Tool {\n\n    public static void show(String msg) {\n        if (BuildConfig.DEBUG) {\n            KLog.e(msg);\n            ToastUtils.show(msg);\n        }\n    }\n\n    public static void printCallStatck() {\n        if (!BuildConfig.DEBUG) {\n            return;\n        }\n        Throwable ex = new Throwable();\n        StackTraceElement[] stackElements = ex.getStackTrace();\n        KLog.e(\"-----------------------------------\");\n        for (StackTraceElement stackElement : stackElements) {\n            KLog.e(stackElement.getClassName() + \"_\" + stackElement.getFileName() + \"_\" + stackElement.getLineNumber() + \"_\" + stackElement.getMethodName());\n        }\n        KLog.e(\"-----------------------------------\");\n    }\n\n    public static void printCallStatck2(Throwable ex) {\n        if (!BuildConfig.DEBUG) {\n            return;\n        }\n        StackTraceElement[] stackElements = ex.getStackTrace();\n        KLog.e(\"-----------------------------------\");\n        for (StackTraceElement stackElement : stackElements) {\n            KLog.e(stackElement.getClassName() + \"_\" + stackElement.getFileName() + \"_\" + stackElement.getLineNumber() + \"_\" + stackElement.getMethodName());\n        }\n        KLog.e(\"-----------------------------------\");\n    }\n\n    public static void setBackgroundColor(View object) {\n        if (App.i().getUser().getThemeMode() == App.THEME_NIGHT) {\n            object.setBackgroundColor(App.i().getResources().getColor(R.color.dark_background));\n        } else {\n            object.setBackgroundColor(App.i().getResources().getColor(R.color.white));\n        }\n    }\n\n//    public static void setWebViewsBGColor() {\n//        if (WithPref.i().getThemeMode() == App.THEME_NIGHT) {\n//            for (WebViewS webViewS : App.i().mWebViewCaches) {\n//                webViewS.setBackgroundColor(App.i().getResources().getColor(R.color.article_dark_background));\n//            }\n//        } else {\n//            for (WebViewS webViewS : App.i().mWebViewCaches) {\n//                webViewS.setBackgroundColor(App.i().getResources().getColor(R.color.white));\n//            }\n//        }\n//    }\n\n\n    public static String getNetFileSizeDescription(Context context, long size) {\n        if (context != null && size == -1) {\n            return context.getString(R.string.unknown);\n        }\n        StringBuilder bytes = new StringBuilder();\n        DecimalFormat format = new DecimalFormat(\"###.0\");\n        if (size >= 1024 * 1024 * 1024) {\n            double i = (size / (1024.0 * 1024.0 * 1024.0));\n            bytes.append(format.format(i)).append(\"GB\");\n        } else if (size >= 1024 * 1024) {\n            double i = (size / (1024.0 * 1024.0));\n            bytes.append(format.format(i)).append(\"MB\");\n        } else if (size >= 1024) {\n            double i = (size / (1024.0));\n            bytes.append(format.format(i)).append(\"KB\");\n        } else {\n            if (size <= 0) {\n                bytes.append(\"0B\");\n            } else {\n                bytes.append((int) size).append(\"B\");\n            }\n        }\n        return bytes.toString();\n    }\n\n\n    /**\n     * 包名判断是否为主进程\n     *\n     * @param context\n     * @return\n     */\n    public static boolean isMainProcess(Context context) {\n        return context.getPackageName().equals(getProcessName(context));\n    }\n\n    /**\n     * 获取进程名称\n     *\n     * @param context\n     * @return\n     */\n    public static String getProcessName(Context context) {\n        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);\n        List<ActivityManager.RunningAppProcessInfo> runningApps = am.getRunningAppProcesses();\n        if (runningApps == null) {\n            return null;\n        }\n        for (ActivityManager.RunningAppProcessInfo proInfo : runningApps) {\n            if (proInfo.pid == android.os.Process.myPid()) {\n                if (proInfo.processName != null) {\n                    return proInfo.processName;\n                }\n            }\n        }\n        return null;\n    }\n\n    /**\n     * 判断某个Activity 界面是否在前台\n     *\n     * @param context\n     * @param className 某个界面名称\n     * @return\n     */\n    public static boolean isForeground(Context context, String className) {\n        if (context == null || TextUtils.isEmpty(className)) {\n            return false;\n        }\n\n        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);\n        List<ActivityManager.RunningTaskInfo> list = am.getRunningTasks(1);\n        if (list != null && list.size() > 0) {\n            ComponentName cpn = list.get(0).topActivity;\n            if (className.equals(cpn.getClassName())) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public static boolean isAppForeground(Context context, String packageName) {\n        if (context == null || TextUtils.isEmpty(packageName)) {\n            return false;\n        }\n        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);\n        List<ActivityManager.RunningAppProcessInfo> appProcesses = am.getRunningAppProcesses();\n\n        if (appProcesses == null)\n            return false;\n        for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {\n            if (appProcess.processName.equals(packageName) && appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n//    public boolean isDebug() {\n//        try {\n//            ApplicationInfo info = this.getApplicationInfo();\n//            return (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;\n//        } catch (Exception e) {\n//            return false;\n//        }\n//    }\n\n\n//    public void clear(int days) {\n//        long clearTime = System.currentTimeMillis() - days * 24 * 3600 * 1000L;\n//        List<Article> allArtsBeforeTime = WithDB.i().getArtInReadedUnstarLtTime(clearTime);\n//        KLog.i(\"清除A：\" + clearTime + \"--\" + allArtsBeforeTime.size() + \"--\" + days);\n//        if (allArtsBeforeTime.size() == 0) {\n//            return;\n//        }\n//        ArrayList<String> idListMD5 = new ArrayList<>(allArtsBeforeTime.size());\n//        for (Article article : allArtsBeforeTime) {\n//            idListMD5.add(StringUtil.MD5(article.getId()));\n//        }\n//        KLog.i(\"清除B：\" + clearTime + \"--\" + allArtsBeforeTime.size() + \"--\" + days);\n//        FileUtil.deleteHtmlDirList(idListMD5);\n//        WithDB.i().delArt(allArtsBeforeTime);\n//        WithDB.i().delArticleImgs(allArtsBeforeTime);\n//    }\n//\n//    private void up(){\n//        List<Article> articles = WithDB.i().getArtsUnhandle();\n//        File file;\n//        File files[];\n//        for (Article article:articles){\n//            file = new File(App.boxReadRelativePath + article.getTitle() + \"_files\") ;\n//            if(file.exists()){\n//                files = file.listFiles();\n//                article.setReadState(Api.ART_READED);\n//                article.setSaveDir(Api.SAVE_DIR_BOXREAD);\n//                article.setImgState(Api.ImgState_Over);\n//                article.setCoverSrc( \"file:\" + File.separator + File.separator + files[0].getAbsolutePath() );\n//                KLog.e(\"该文件存在\"+  \"==\" + files[0].getAbsolutePath()  );\n//            }\n//        }\n//        WithDB.i().saveArticles(articles);\n//    }\n//\n//    private void up3(){\n//        File files[] = new File(App.boxRelativePath).listFiles();\n//        int size = 0;\n//        File DirFile;\n//\n//        String fileName;\n//        for (File file:files){\n//            if(!file.getUserName().endsWith(\".html\")){\n//                continue;\n//            }\n//            fileName = file.getUserName().replace(\".html\",\"\");\n//            if(WithDB.i().isArticleExists(fileName)){\n//                continue;\n//            }\n//            size++;\n//            KLog.e(\"该文章在数据库中不存在：\" + fileName );\n//            FileUtil.moveFile(file.getPath(), FileUtil.getRelativeDir(\"VV\") + fileName + \".html\" );\n////            KLog.e(\"移动文件：\" + file.getPath() + \" = \" +  FileUtil.getRelativeDir(\"VV\") + fileName + \".html\"  );\n//            DirFile = new File( App.boxRelativePath + fileName + \"_files\" );\n//            if(DirFile.exists()){\n//                FileUtil.moveDir(DirFile.getPath(), FileUtil.getRelativeDir(\"VV\") + fileName + \"_files\" );\n////                KLog.e(\"移动目录：\" + DirFile.getPath() + \" = \" +  FileUtil.getRelativeDir(\"VV\") + fileName + \"_files\"  );\n//            }\n//        }\n//        KLog.e(\"不存在的文件数为：\" + size );\n//    }\n//\n//    private void up2(){\n//        List<Article> articles = WithDB.i().loadAllArts();\n//        for (Article article:articles){\n//            article.setReadState(Api.ART_UNREAD);\n//            if(!article.getSaveDir().equals(Api.NOT_FILED)){\n//                article.setReadState(Api.ART_UNREADING);\n//            }\n//        }\n//        WithDB.i().saveArticles(articles);\n//    }\n//\n//\n//    public static InoApi getNetApi(){\n//        return InoApi.i();\n//    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/UriUtil.java",
    "content": "package me.wizos.loread.utils;\n\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport android.webkit.URLUtil;\n\nimport com.socks.library.KLog;\n\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLDecoder;\n\npublic class UriUtil {\n    public static String getBaseUrl(String url){\n        Uri uri = Uri.parse(url);\n        return uri.getScheme() + \"://\" + uri.getHost();\n    }\n\n    // https://www.hao123.com/favicon.ico\n    public static String getFaviconUrl(String url){\n        Uri uri = Uri.parse(url);\n        return uri.getScheme() + \"://\" + uri.getHost() + \"/favicon.ico\";\n    }\n\n    public static String guessFileName(String url, String contentDisposition, String mimeType) {\n        String fileNameByGuess;\n        // 处理会把 epub 文件，识别为 bin 文件的 bug：https://blog.csdn.net/imesong/article/details/45568697\n        if (\"application/octet-stream\".equals(mimeType)) {\n            if (TextUtils.isEmpty(contentDisposition)) {\n                // 从路径中获取\n                fileNameByGuess = guessFileNameExt(url);\n            } else {\n                fileNameByGuess = contentDisposition.substring(contentDisposition.indexOf(\"filename=\") + 9);\n                if(fileNameByGuess.startsWith(\"\\'\") && fileNameByGuess.length()> 1){\n                    fileNameByGuess = fileNameByGuess.substring(1);\n                }\n                if(fileNameByGuess.endsWith(\"\\'\") && fileNameByGuess.length()> 1){\n                    fileNameByGuess = fileNameByGuess.substring(0,fileNameByGuess.length()-1);\n                }\n                if(fileNameByGuess.startsWith(\"\\\"\") && fileNameByGuess.length()> 1){\n                    fileNameByGuess = fileNameByGuess.substring(1);\n                }\n                if(fileNameByGuess.endsWith(\"\\\"\") && fileNameByGuess.length()> 1){\n                    fileNameByGuess = fileNameByGuess.substring(0,fileNameByGuess.length()-1);\n                }\n            }\n        }else {\n            fileNameByGuess = URLUtil.guessFileName(url, contentDisposition, mimeType);\n        }\n\n        KLog.i(\"猜测的文件名为：\" + mimeType + \" -- \" + fileNameByGuess + \" -- \" + contentDisposition );\n        // 处理 url 中包含乱码中文的问题\n        try {\n            fileNameByGuess = URLDecoder.decode(fileNameByGuess, \"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            e.printStackTrace();\n        }\n\n        return fileNameByGuess;\n    }\n\n    /**\n     * 从 url 中获取文件名(含后缀)\n     *\n     * @param url 网址\n     * @return 文件名(含后缀)\n     */\n    public static String guessFileNameExt(String url) {\n        if (TextUtils.isEmpty(url)) {\n            return \"\";\n        }\n        String fileName,param = \"\";\n        int end = url.lastIndexOf(\"?\");\n        if( end < 0 ){\n            end = url.length();\n        }else {\n            param = SymbolUtil.filterUnsavedSymbol(url.substring(end+1));\n            if( !TextUtils.isEmpty(param) ){\n                param = param + \"_\";\n            }\n        }\n        int start = url.lastIndexOf(\"/\",end);\n        fileName = param + url.substring(start + 1, end);\n\n        // 减少文件名太长的情况\n        if (fileName.length() > 64) {\n            fileName = fileName.substring(fileName.length() - 64);\n        }\n\n        return FileUtil.getSaveableName(fileName);\n    }\n\n\n    public static String guessImageSuffix(String url) {\n        int typeIndex = url.lastIndexOf(\".\");\n        String fileExt = url.substring(typeIndex, url.length());\n        if (fileExt.contains(\".jpg\")) {\n            url = url.substring(0, typeIndex) + \".jpg\";\n        } else if (fileExt.contains(\".jpeg\")) {\n            url = url.substring(0, typeIndex) + \".jpeg\";\n        } else if (fileExt.contains(\".png\")) {\n            url = url.substring(0, typeIndex) + \".png\";\n        } else if (fileExt.contains(\".gif\")) {\n            url = url.substring(0, typeIndex) + \".gif\";\n        }\n        KLog.d(\"【 修正后的url 】\" + url);\n        return url;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/utils/VideoInjectUtil.java",
    "content": "package me.wizos.loread.utils;\n\nimport me.wizos.loread.bridge.WebBridge;\n\n/**\n * @author by Wizos on 2018/6/29.\n */\n\npublic class VideoInjectUtil {\n    /**\n     * 注入全屏Js，对不同的视频网站分析相应的全屏控件——class标识\n     *\n     * @param url 加载的网页地址\n     * @return 注入的js内容，若不是需要适配的网址则返回空javascript\n     */\n    public static String fullScreenJsFun(String url) {\n        String fullScreenFlags = null;\n        // http://v.qq.com/txp/iframe/player.html?vid=v0151eygqka、http://v.qq.com/iframe/player.html?vid=v0151eygqka\n        if (url.contains(\"qq.com/txp/iframe/player.html\")) {\n            fullScreenFlags = \"txp_btn_fullscreen\";\n        } else if (url.contains(\"sohu\")) {\n            fullScreenFlags = \"x-fs-btn\";\n        } else if (url.contains(\"letv\")) {\n            fullScreenFlags = \"hv_ico_screen\";\n        }\n        if (null != fullScreenFlags) {\n            return \"javascript:document.getElementsByClassName('\" + fullScreenFlags + \"')[0].addEventListener('click',function(){\" + WebBridge.TAG + \".toggleScreenOrientation();return false;});\";\n        } else {\n            return \"javascript:\";\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/ExpandableListViewS.java",
    "content": "//package me.wizos.loreadx.view;\n//\n//import android.content.Context;\n//import android.graphics.Canvas;\n//import android.util.AttributeSet;\n//import android.util.Log;\n//import android.view.MotionEvent;\n//import android.view.View;\n//import android.view.ViewGroup;\n//import android.widget.AbsListView;\n//import android.widget.ExpandableListAdapter;\n//import android.widget.ExpandableListView;\n//\n//import com.socks.library.KLog;\n//\n//import java.util.ArrayList;\n//import java.util.Iterator;\n//import java.util.List;\n//\n///**\n// * @author Wizos on 2017/9/17.\n// */\n//\n//public class ExpandableListViewS extends ExpandableListView implements AbsListView.OnScrollListener { // PinnedHeader\n//    public ExpandableListViewS(Context context) {\n//        super(context);\n//        setOnScrollListener(this);\n//    }\n//\n//    public ExpandableListViewS(Context context, AttributeSet attrs) {\n//        super(context, attrs);\n//        setOnScrollListener(this);\n//    }\n//\n//    public ExpandableListViewS(Context context, AttributeSet attrs, int defStyleAttr) {\n//        super(context, attrs, defStyleAttr);\n//        setOnScrollListener(this);\n//    }\n//\n//\n//    /**\n//     * Adapter 接口 . 列表必须实现此接口 .\n//     */\n//    private HeaderAdapter mAdapter;\n//\n//    public interface HeaderAdapter {\n//        int PINNED_HEADER_GONE = 0;\n//        int PINNED_HEADER_VISIBLE = 1;\n//        int PINNED_HEADER_PUSHED_UP = 2;\n//\n//        /**\n//         * 获取 Header 的状态\n//         *\n//         * @return STICKY_HEADER_GONE, STICKY_HEADER_VISIBLE, STICKY_HEADER_PUSHED_UP 其中之一\n//         */\n//        int getHeaderState(int groupPosition, int childPosition);\n//\n//        /**\n//         * 配置 Header, 让 Header 知道显示的内容\n//         */\n//        void configureHeader(View header, int groupPosition, int childPosition, int alpha);\n//    }\n//\n//\n//    /**\n//     * 用于在列表头显示的 View,mHeaderViewVisible 为 true 才可见\n//     */\n//    private View mHeaderView;\n//\n//    /**\n//     * 列表头是否可见\n//     */\n//    private boolean mHeaderViewVisible;\n//\n//    private static final int MAX_ALPHA = 255;\n//    private int mHeaderViewWidth;\n//    private int mHeaderViewHeight;\n//\n//    public void setPinnedHeaderView(View view) {\n//        mHeaderView = view;\n//        AbsListView.LayoutParams lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);\n//        view.setLayoutParams(lp);\n//\n//        if (mHeaderView != null) {\n//            setFadingEdgeLength(0);\n//        }\n//        requestLayout();\n//    }\n//\n//\n//    private OnPinnedGroupClickListener mOnPinnedGroupClickListener;\n//\n//    public void setOnPinnedGroupClickListener(OnPinnedGroupClickListener onPinnedGroupClickListener) {\n//        mOnPinnedGroupClickListener = onPinnedGroupClickListener;\n//    }\n//\n//    public interface OnPinnedGroupClickListener {\n//        void onHeaderClick(ExpandableListView parent, View v, int pinnedGroupPosition);\n//    }\n//\n//    private float mDownX;\n//    private float mDownY;\n//\n//    /**\n//     * 如果 HeaderView 是可见的 , 此函数用于判断是否点击了 HeaderView, 并对做相应的处理 , 因为 HeaderView\n//     * 是画上去的 , 所以设置事件监听是无效的 , 只有自行控制 .\n//     */\n//    @Override\n//    public boolean onTouchEvent(MotionEvent ev) {\n//\n//        if (canScrollVertically(this)) {\n//            getParent().requestDisallowInterceptTouchEvent(true);\n//        }\n//        if (mHeaderViewVisible) {\n//            switch (ev.getAction()) {\n//                case MotionEvent.ACTION_DOWN:\n//                    mDownX = ev.getX();\n//                    mDownY = ev.getY();\n//                    if (mDownX <= mHeaderViewWidth && mDownY <= mHeaderViewHeight) {\n//                        return true;\n//                    }\n//                    break;\n//                case MotionEvent.ACTION_UP:\n//                    float x = ev.getX();\n//                    float y = ev.getY();\n//                    float offsetX = Math.abs(x - mDownX);\n//                    float offsetY = Math.abs(y - mDownY);\n//\n//                    // 如果 HeaderView 是可见的 , 点击在 HeaderView 内 , 那么触发 headerClick()\n//                    if (x <= mHeaderViewWidth && y <= mHeaderViewHeight && offsetX <= mHeaderViewWidth\n//                            && offsetY <= mHeaderViewHeight) {\n//                        if (mHeaderView != null && mOnPinnedGroupClickListener != null) {\n//                            long packedPosition = getExpandableListPosition(this.getFirstVisiblePosition());\n//                            int pinnedGroupPosition = ExpandableListView.getPackedPositionGroup(packedPosition);\n//                            mOnPinnedGroupClickListener.onHeaderClick(this, mHeaderView, pinnedGroupPosition);\n//                        }\n//                        return true;\n//                    }\n//\n//                    break;\n//                default:\n//                    break;\n//            }\n//        }\n//        return super.onTouchEvent(ev);\n//    }\n//\n//    @Override\n//    public void setAdapter(ExpandableListAdapter adapter) {\n//        super.setAdapter(adapter);\n//        mAdapter = (HeaderAdapter) adapter;\n//    }\n//\n//    @Override\n//    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n//        if (mHeaderView != null) {\n//            measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);\n//            mHeaderViewWidth = mHeaderView.getMeasuredWidth();\n//            mHeaderViewHeight = mHeaderView.getMeasuredHeight();\n//        }\n//    }\n//\n//    private int mOldState = -1;\n//\n//    @Override\n//    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n//        super.onLayout(changed, left, top, right, bottom);\n//        final long flatPostion = getExpandableListPosition(getFirstVisiblePosition());\n//        final int groupPos = ExpandableListView.getPackedPositionGroup(flatPostion);\n//        final int childPos = ExpandableListView.getPackedPositionChild(flatPostion);\n//        int state = mAdapter.getHeaderState(groupPos, childPos);\n//        if (mHeaderView != null && mAdapter != null && state != mOldState) {\n//            mOldState = state;\n//            mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);\n//        }\n//        configureHeaderView(groupPos, childPos);\n//    }\n//\n//    public void configureHeaderView(int groupPosition, int childPosition) {\n//        if (mHeaderView == null || mAdapter == null || ((ExpandableListAdapter) mAdapter).getGroupCount() == 0) {\n//            return;\n//        }\n//        int state = mAdapter.getHeaderState(groupPosition, childPosition);\n//        switch (state) {\n//            case HeaderAdapter.PINNED_HEADER_GONE: {\n//                mHeaderViewVisible = false;\n//                Log.e(\"粘连布局：\", \"忽略: \" + \" [\" + groupPosition + \",\" + childPosition + \"] , \" + mOldState + \" , [\" + mHeaderViewHeight + \",\" + mHeaderViewWidth + \"]\");\n//\n//                break;\n//            }\n//            case HeaderAdapter.PINNED_HEADER_VISIBLE: {\n//                mAdapter.configureHeader(mHeaderView, groupPosition, childPosition, MAX_ALPHA);\n//                Log.e(\"粘连布局：\", \"可见: \" + \" [\" + groupPosition + \",\" + childPosition + \"] , \" + mOldState + \" , [\" + mHeaderViewHeight + \",\" + mHeaderViewWidth + \"]   \" + MAX_ALPHA + \"  \" + mHeaderView.getTop());\n//\n//                if (mHeaderView.getTop() != 0) {\n//                    mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);\n//                }\n//                mHeaderViewVisible = true;\n//                break;\n//            }\n//            case HeaderAdapter.PINNED_HEADER_PUSHED_UP: {\n//                View firstView = getChildAt(0);\n//                int bottom = firstView.getBottom();\n////                int itemHeight = firstView.getHeight();\n//                int headerHeight = mHeaderView.getHeight();\n//                int y;\n//                int alpha;\n//                if (bottom < headerHeight) {\n//                    y = (bottom - headerHeight);\n//                    alpha = MAX_ALPHA * (headerHeight + y) / headerHeight;\n//                } else {\n//                    y = 0;\n//                    alpha = MAX_ALPHA;\n//                }\n//                mAdapter.configureHeader(mHeaderView, groupPosition, childPosition, alpha);\n//                if (mHeaderView.getTop() != y) {\n//                    mHeaderView.layout(0, y, mHeaderViewWidth, mHeaderViewHeight + y);\n//                }\n//                Log.e(\"粘连布局：\", \"推动: \" + \" [\" + groupPosition + \",\" + childPosition + \"] , \" + mOldState + \" , [\" + mHeaderViewHeight + \",\" + mHeaderViewWidth + \"]   \" + MAX_ALPHA + \"  \" + mHeaderView.getTop());\n//\n//                mHeaderViewVisible = true;\n//                break;\n//            }\n//        }\n//    }\n//\n//    /**\n//     * 列表界面更新时调用该方法(如滚动时)\n//     */\n//    @Override\n//    protected void dispatchDraw(Canvas canvas) {\n//        super.dispatchDraw(canvas);\n//        KLog.e(\"绘制到页面\" + mHeaderViewVisible);\n//        if (mHeaderViewVisible) {\n//            // 分组栏是直接绘制到界面中，而不是加入到ViewGroup中\n//            drawChild(canvas, mHeaderView, getDrawingTime());\n//        }\n//    }\n//\n//    @Override\n//    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {\n//        final long flatPos = getExpandableListPosition(firstVisibleItem);\n//        int groupPosition = ExpandableListView.getPackedPositionGroup(flatPos);\n//        int childPosition = ExpandableListView.getPackedPositionChild(flatPos);\n//        Log.e(\"展开\", \"滚动：\" + firstVisibleItem + \", \" + flatPos + \", \" + groupPosition + \", \" + childPosition);\n//        configureHeaderView(groupPosition, childPosition);\n//    }\n//\n//    @Override\n//    public void onScrollStateChanged(AbsListView view, int scrollState) {\n//    }\n//\n//    public boolean canScrollVertically(AbsListView view) {\n//        boolean canScroll = false;\n//\n//        if (view != null && view.getChildCount() > 0) {\n//            boolean isOnTop = view.getFirstVisiblePosition() != 0 || view.getChildAt(0).getTop() != 0;\n//            boolean isAllItemsVisible = isOnTop && view.getLastVisiblePosition() == view.getChildCount();\n//\n//            if (isOnTop || isAllItemsVisible) {\n//                canScroll = true;\n//            }\n//        }\n//\n//        return canScroll;\n//    }\n//\n//    public void setAssociatedListView(AbsListView listView) {\n//        listView.setOnScrollListener(associatedListViewListener);\n//        updateListViewScrollState(listView);\n//    }\n//\n//    private final AbsListView.OnScrollListener associatedListViewListener =\n//            new AbsListView.OnScrollListener() {\n//                @Override\n//                public void onScrollStateChanged(AbsListView view, int scrollState) {\n//                    updateListViewScrollState(view);\n//                }\n//\n//                @Override\n//                public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {\n//                    updateListViewScrollState(view);\n//                }\n//            };\n//\n//    private void updateListViewScrollState(AbsListView listView) {\n//        if (listView.getChildCount() == 0) {\n//            isDraggable = true;\n//        } else {\n//            if (listView.getFirstVisiblePosition() == 0) {\n//                View firstChild = listView.getChildAt(0);\n//                if (firstChild.getTop() == listView.getPaddingTop()) {\n//                    isDraggable = true;\n//                    return;\n//                }\n//            }\n//            isDraggable = false;\n//        }\n//    }\n//\n//    private boolean isDraggable = true;\n//    // 可拖动\n//\n//\n//    // 以上都是 设置 PinnedGroup 的内容\n//\n//\n//    /**\n//     * 实现/接管 AbsListView.OnScrollListener 接口，让 setOnScrollListener 改造为 addOnScrollListener，可以添加更多的监听器。\n//     * 因为要实现“悬停抽屉只有在其内部的listview到达最顶部的时候，才能下拉”和“PinnedGroup”功能，都要添加 OnScrollListener 监听器。\n//     */\n//    private final CompositeScrollListener compositeScrollListener = new CompositeScrollListener();\n//\n//    {\n//        //    其实再类内部{}只是代表在调用构造函数之前在{}中初始化，static{}只在类加载时调用\n//        //    new子类的对象时，先调用父类staic{}里的东西，在调用子类里的static{}，在调用父类{}的在调用父类构造方法，在调用子类构造方法\n//        //    调用子类或者父类的静态方法时，先调用父类的static{}在调用子类的static{}\n//        super.setOnScrollListener(compositeScrollListener);\n//    }\n//\n//    /**\n//     * 添加一个OnScrollListener,不会取代已添加OnScrollListener\n//     * <p><b>Make sure call this on UI thread</b></p>\n//     *\n//     * @param listener the listener to add\n//     */\n//    @Override\n//    public void setOnScrollListener(final OnScrollListener listener) {\n//        addOnScrollListener(listener);\n//    }\n//\n//    /**\n//     * 添加一个OnScrollListener,不会取代已添加OnScrollListener\n//     * <p><b>Make sure call this on UI thread</b></p>\n//     *\n//     * @param listener the listener to add\n//     */\n//    public void addOnScrollListener(final OnScrollListener listener) {\n////        throwIfNotOnMainThread();\n//        compositeScrollListener.addOnScrollListener(listener);\n//    }\n//\n//    //\n////    /**\n////     * 删除前一个添加scrollListener,只会删除完全相同的对象\n////     * <p><b>Make sure call this on UI thread.</b></p>\n////     *\n////     * @param listener the listener to remove\n////     */\n////    public void removeOnScrollListener(final OnScrollListener listener) {\n//////        throwIfNotOnMainThread();\n////        compositeScrollListener.removeOnScrollListener(listener);\n////    }\n//    private class CompositeScrollListener implements OnScrollListener {\n//        private final List<OnScrollListener> scrollListenerList = new\n//                ArrayList<OnScrollListener>();\n//\n//        public void addOnScrollListener(OnScrollListener listener) {\n//            if (listener == null) {\n//                return;\n//            }\n//            for (OnScrollListener scrollListener : scrollListenerList) {\n//                if (listener == scrollListener) {\n//                    return;\n//                }\n//            }\n//            scrollListenerList.add(listener);\n//        }\n//\n//        public void removeOnScrollListener(OnScrollListener listener) {\n//            if (listener == null) {\n//                return;\n//            }\n//            Iterator<OnScrollListener> iterator = scrollListenerList.iterator();\n//            while (iterator.hasNext()) {\n//                OnScrollListener scrollListener = iterator.next();\n//                if (listener == scrollListener) {\n//                    iterator.remove();\n//                    return;\n//                }\n//            }\n//        }\n//\n//        @Override\n//        public void onScrollStateChanged(AbsListView view, int scrollState) {\n//            List<OnScrollListener> listeners = new ArrayList<OnScrollListener>(scrollListenerList);\n//            for (OnScrollListener listener : listeners) {\n//                listener.onScrollStateChanged(view, scrollState);\n//            }\n//        }\n//\n//        @Override\n//        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {\n//            List<OnScrollListener> listeners = new ArrayList<OnScrollListener>(scrollListenerList);\n//            for (OnScrollListener listener : listeners) {\n//                listener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);\n//            }\n//        }\n//    }\n//}\n//\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/FriendlyCardView.java",
    "content": "/*\n * Copyright (c) 2015 Zhang Hai <Dreaming.in.Code.ZH@Gmail.com>\n * All Rights Reserved.\n */\n\npackage me.wizos.loread.view;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.util.AttributeSet;\n\nimport androidx.cardview.widget.CardView;\n\nimport me.wizos.loread.R;\n\n\n/**\n * A friendly card view that has consistent padding across API levels.\n */\npublic class FriendlyCardView extends CardView {\n\n    public FriendlyCardView(Context context) {\n        super(context);\n\n        init(getContext(), null, 0);\n    }\n\n    public FriendlyCardView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n\n        init(getContext(), attrs, 0);\n    }\n\n    public FriendlyCardView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n\n        init(getContext(), attrs, defStyleAttr);\n    }\n\n    private void init(Context context, AttributeSet attrs, int defStyleAttr) {\n\n        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr,\n                R.style.CardView_Light);\n        setMaxCardElevation(a.getDimension(R.styleable.CardView_cardMaxElevation,\n                getCardElevation()));\n        a.recycle();\n\n        setUseCompatPadding(true);\n        setPreventCornerOverlap(false);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/IconFontView.java",
    "content": "package me.wizos.loread.view;\n\nimport android.content.Context;\nimport android.graphics.Typeface;\nimport android.util.AttributeSet;\n\nimport androidx.appcompat.widget.AppCompatTextView;\n\n\n/**\n * Created by Wizos on 2016/11/6.\n */\n\npublic class IconFontView extends AppCompatTextView {\n\n    public IconFontView(Context context) {\n        super(context);\n        init();\n    }\n\n    public IconFontView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init();\n    }\n\n    public IconFontView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init();\n    }\n\n    private void init() {\n        Typeface iconFont = Typeface.createFromAsset(getContext().getAssets(), \"iconfont.ttf\");\n        this.setTypeface(iconFont);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/SwipeRefreshLayoutS.java",
    "content": "package me.wizos.loread.view;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewConfiguration;\nimport android.widget.AbsListView;\n\nimport androidx.swiperefreshlayout.widget.SwipeRefreshLayout;\n\n/**\n * 下拉刷新控件的包装。解决下拉和左右滑动冲突的问题\n * Created by Wizos on 2016/3/30.\n */\npublic class SwipeRefreshLayoutS extends SwipeRefreshLayout {\n    private View view;\n\n    // 方案2：解决下来刷新与左右滑动的冲突\n    private int mTouchSlop;\n    private float mPrevX;\n\n    public SwipeRefreshLayoutS(Context context) {\n        super(context);\n    }\n\n    public SwipeRefreshLayoutS(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        // 触发移动事件的最短距离，如果小于这个距离就不触发移动控件\n        // 判断用户在进行滑动操作的最小距离\n        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();\n    }\n\n    public void setViewGroup(View view) {\n        this.view = view;\n    }\n\n    /**\n     * 当SwipeRefreshLayout 不只有 listView一个子view时，向下滑动的时候就会出现还没有滑倒listview顶部就触发下拉刷新的动作。\n     * 看SwipeRefreshLayout源码可以看到在onInterceptTouchEvent里面有这样的一段代码\n     * if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing) {\n     * // Fail fast if we're not in a state where a swipe is possible\n     * return false;\n     * }\n     * <p>\n     * 其中有个canChildScrollUp方法，在往下看\n     * public boolean canChildScrollUp() {\n     * if (android.os.Build.VERSION.SDK_INT < 14) {\n     * if (mTarget instanceof AbsListView) {\n     * final AbsListView absListView = (AbsListView) mTarget;\n     * return absListView.getChildCount() > 0\n     * && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)\n     * .getTop() < absListView.getPaddingTop());\n     * } else {\n     * return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;\n     * }\n     * } else {\n     * return ViewCompat.canScrollVertically(mTarget, -1);\n     * }\n     * }\n     * 决定子view 能否滑动就是在这里了，所以我们只有写一个类继承SwipeRefreshLayout，然后重写该方法即可\n     */\n    @Override\n    public boolean canChildScrollUp() {\n        if (view != null && view instanceof AbsListView) {\n            final AbsListView absListView = (AbsListView) view;\n            return absListView.getChildCount() > 0\n                    && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)\n                    .getTop() < absListView.getPaddingTop());\n        }\n        return super.canChildScrollUp();\n    }\n\n\n    /**\n     * 作者：秋天的雨滴\n     * 链接：https://www.jianshu.com/p/04d799608c2e\n     * 解决使用该view（SwipeRefreshLayout）下拉刷新和子view（ViewPager）左右滑动事件冲突的问题\n     * 使用 SwipeRefreshLayout， 左右滑动 listView item 会出现卡顿，停滞现象，究其原因，是左右滑动和下拉刷新（垂直）冲突导致。\n     * 就是SwipeRefreshLayout对于Y 轴的处理容差值很小，如果不是水平滑动，很轻易就会触发下拉刷新。\n     * 为了解决该问题，需要重写SwipeRefreshLayout的onInterceptTouchEvent(MotionEvent ev)事件，在这里面进行处理，当X距离滑动大于某个值时，就认为是左右滑动，不执行下拉刷新操作。\n     */\n    @Override\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n        switch (ev.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                mPrevX = MotionEvent.obtain(ev).getX();\n                break;\n            case MotionEvent.ACTION_MOVE:\n                final float eventX = ev.getX();\n                //获取水平移动距离\n                float xDiff = Math.abs(eventX - mPrevX);\n                //当水平移动距离大于滑动操作的最小距离的时候就认为进行了横向滑动\n                //不进行事件拦截,并将这个事件交给子View处理\n                if (xDiff > mTouchSlop) {\n                    return false;\n                }\n            default:\n                break;\n        }\n        return super.onInterceptTouchEvent(ev);\n    }\n\n    @Override\n    public boolean onTouchEvent(MotionEvent ev) {\n//        KLog.e(\"事件传递到了 下拉控件的 onTouchEvent\");\n        return super.onTouchEvent(ev);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/WebViewS.java",
    "content": "package me.wizos.loread.view;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.webkit.CookieManager;\nimport android.webkit.WebSettings;\n\nimport com.socks.library.KLog;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.view.webview.NestedScrollWebView;\n\n/**\n * @author Wizos on 2017/7/3.\n * NestedScroll\n */\n\n\npublic class WebViewS extends NestedScrollWebView {\n    private boolean isReadability = false;\n//    private int downX,downY;\n\n    @SuppressLint(\"NewApi\")\n    public WebViewS(final Context context) {\n        // 传入 application activity 来防止 activity 引用被滥用。\n        // 创建 WebView 传的是 Application ， Application 本身是无法弹 Dialog 的 。 所以只能无反应 ！\n        // 这个问题解决方案只要你创建 WebView 时候传入 Activity ， 或者 自己实现 onJsAlert 方法即可。\n        super(context);\n        initSettingsForWebPage();\n        // 获取手指点击事件的xy坐标\n//        setOnTouchListener( new View.OnTouchListener() {\n//            @Override\n//            public boolean onTouch(View arg0, MotionEvent arg1) {\n//                downX = (int) arg1.getX();\n//                downY = (int) arg1.getY();\n//                return false;\n//            }\n//        });\n//        作者：Wing_Li\n//        链接：https://www.jianshu.com/p/3fcf8ba18d7f\n//        setOnLongClickListener(new View.OnLongClickListener() {\n//            @Override\n//            public boolean onLongClick(View v) {\n//                if (!BuildConfig.DEBUG) {\n//                    return false;\n//                }\n//                final HitTestResult status = ((WebViewS) v).getHitTestResult();\n//                if (null == status) {\n//                    return false;\n//                }\n//                int type = status.getType();\n//                if (type == WebView.HitTestResult.UNKNOWN_TYPE) {\n//                    return false;\n//                }\n//\n//                // 这里可以拦截很多类型，我们只处理图片类型就可以了\n//                switch (type) {\n//                    case WebView.HitTestResult.PHONE_TYPE: // 处理拨号\n//                        Tool.show(\"长按手机号\");\n//                        break;\n//                    case WebView.HitTestResult.EMAIL_TYPE: // 处理Email\n//                        Tool.show(\"长按邮件\");\n//                        break;\n//                    case WebView.HitTestResult.GEO_TYPE: // 地图类型\n//                        Tool.show(\"长按地图\");\n//                        break;\n//                    case WebView.HitTestResult.SRC_ANCHOR_TYPE: // 超链接\n//                        final LongClickPopWindow webViewLongClickedPopWindow = new LongClickPopWindow(context,WebView.HitTestResult.SRC_ANCHOR_TYPE, ScreenUtil.dp2px(context,120), ScreenUtil.dp2px(context,90));\n//                        webViewLongClickedPopWindow.showAtLocation(v, Gravity.TOP|Gravity.LEFT, downX, downY + 10);\n//\n//                        webViewLongClickedPopWindow.getView(R.id.webview_copy_link)\n//                                .setOnClickListener(new View.OnClickListener() {\n//                                    @Override\n//                                    public void onClick(View v) {\n//                                        webViewLongClickedPopWindow.dismiss();\n//                                        //获取剪贴板管理器：\n//                                        ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);\n//                                        // 创建普通字符型ClipData\n//                                        ClipData mClipData = ClipData.newPlainText(\"url\", status.getExtra());\n//                                        // 将ClipData内容放到系统剪贴板里。\n//                                        cm.setPrimaryClip(mClipData);\n//                                        ToastUtil.showLong(\"复制成功\");\n//                                    }\n//                                });\n//                        webViewLongClickedPopWindow.getView(R.id.webview_share_link)\n//                                .setOnClickListener(new View.OnClickListener() {\n//                                    @Override\n//                                    public void onClick(View v) {\n//                                        webViewLongClickedPopWindow.dismiss();\n//                                        Intent sendIntent = new Intent(Intent.ACTION_SEND);\n//                                        sendIntent.setType(\"text/plain\");\n//                                        sendIntent.putExtra(Intent.EXTRA_TEXT, status.getExtra());\n//                                        sendIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n//                                        context.startActivity(Intent.createChooser(sendIntent, \"分享到\"));\n//                                    }\n//                                });\n//\n//\n////                        SnackbarUtil.Long(WebViewS.this.getRootView(), \"链接：\" + status.getExtra())\n////                                .setAction(\"复制\", new View.OnClickListener() {\n////                                    @Override\n////                                    public void onClick(View v) {\n////                                        //获取剪贴板管理器：\n////                                        ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);\n////                                        // 创建普通字符型ClipData\n////                                        ClipData mClipData = ClipData.newPlainText(\"url\", status.getExtra());\n////                                        // 将ClipData内容放到系统剪贴板里。\n////                                        cm.setPrimaryClip(mClipData);\n////                                        ToastUtil.showLong(\"复制成功\");\n////                                    }\n////                                }).show();\n//                        break;\n//                    case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE: // 一个SRC_IMAGE_ANCHOR_TYPE类型表明了一个拥有图片为子对象的超链接。\n//                        Tool.show(\"长按图片：\" + status.getExtra());\n//                        break;\n//                    case WebView.HitTestResult.IMAGE_TYPE: // 处理长按图片的菜单项\n//                        // 获取图片的路径\n//                        String saveImgUrl = status.getExtra();\n//                        Tool.show(\"长按图片：\" + saveImgUrl);\n//                        // 跳转到图片详情页，显示图片\n//                        break;\n//                    default:\n//                        break;\n//                }\n//                return true;\n//            }\n//        });\n    }\n\n\n    public boolean isReadability() {\n        return isReadability;\n    }\n\n    public void setReadability(boolean readability) {\n        isReadability = readability;\n    }\n\n    // 忽略 SetJavaScriptEnabled 的报错\n    @SuppressLint(\"SetJavaScriptEnabled\")\n    private void initSettingsForWebPage() {\n        setHorizontalScrollBarEnabled(false);\n        setVerticalScrollBarEnabled(false);\n        setScrollbarFadingEnabled(false);\n\n        // 先设置背景色为tranaparent 透明色\n        setBackgroundColor(0);\n        WebSettings webSettings = this.getSettings();\n\n        webSettings.setTextZoom(100);\n        // 设置最小的字号，默认为8\n        webSettings.setMinimumFontSize(10);\n        // 设置最小的本地字号，默认为8\n        webSettings.setMinimumLogicalFontSize(10);\n\n        // 设置自适应屏幕，两者合用\n        // 设置使用 宽 的 Viewpoint,默认是false\n        // Android browser以及chrome for Android的设置是`true`，而WebView的默认设置是`false`\n        // 如果设置为`true`,那么网页的可用宽度为`980px`,并且可以通过 meta data来设置，如果设置为`false`,那么可用区域和WebView的显示区域有关.\n        // 设置此属性，可任意比例缩放\n        webSettings.setUseWideViewPort(true);\n        // 缩放至屏幕的大小：如果webview内容宽度大于显示区域的宽度,那么将内容缩小,以适应显示区域的宽度, 默认是false\n        webSettings.setLoadWithOverviewMode(true);\n        // NARROW_COLUMNS 适应内容大小 ， SINGLE_COLUMN 自适应屏幕\n        webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);\n\n        //缩放操作\n        webSettings.setSupportZoom(true); //支持缩放，默认为true。是下面那个的前提。\n        webSettings.setBuiltInZoomControls(true); //设置内置的缩放控件。若为false，则该WebView不可缩放\n        webSettings.setDisplayZoomControls(false); //隐藏原生的缩放控件\n\n\n        webSettings.setDefaultTextEncodingName(\"UTF-8\");\n\n        webSettings.setJavaScriptEnabled(true);\n        // 支持通过js打开新的窗口\n        webSettings.setJavaScriptCanOpenWindowsAutomatically(false);\n\n        /* 缓存 */\n        webSettings.setDomStorageEnabled(true); // 临时简单的缓存（必须保留，否则无法播放优酷视频网页，其他的可以）\n        webSettings.setAppCacheEnabled(true); // 支持H5的 application cache 的功能\n        webSettings.setDatabaseEnabled(true);  // 支持javascript读写db\n        /* 根据cache-control获取数据 */\n//        webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);\n        webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);\n\n        // 通过 file url 加载的 Javascript 读取其他的本地文件 .建议关闭\n        webSettings.setAllowFileAccessFromFileURLs(false);\n        // 允许通过 file url 加载的 Javascript 可以访问其他的源，包括其他的文件和 http，https 等其他的源\n        webSettings.setAllowUniversalAccessFromFileURLs(false);\n        // 允许访问文件\n        webSettings.setAllowFileAccess(true);\n        // 保存密码数据\n        webSettings.setSavePassword(true);\n        // 保存表单数据\n        webSettings.setSaveFormData(true);\n\n        CookieManager instance = CookieManager.getInstance();\n        instance.setAcceptCookie(true);\n        instance.setAcceptThirdPartyCookies(this, true);\n        // 允许在Android 5.0上 Webview 加载 Http 与 Https 混合内容。作者：Wing_Li，链接：https://www.jianshu.com/p/3fcf8ba18d7f\n        webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);\n\n\n        webSettings.setMediaPlaybackRequiresUserGesture(true);\n\n        webSettings.setRenderPriority(WebSettings.RenderPriority.HIGH);\n\n        setLayerType(View.LAYER_TYPE_HARDWARE, null);\n//        setLayerType(View.LAYER_TYPE_SOFTWARE, null);//开启软件加速（在我的MX5上，滑动时会卡顿）\n\n        // 设置WebView是否应加载图像资源。请注意，此方法控制所有图像的加载，包括使用数据URI方案嵌入的图像。\n        webSettings.setLoadsImagesAutomatically(true);\n        // 将图片下载阻塞，然后在浏览器的OnPageFinished事件中设置webView.getSettings().setBlockNetworkImage(false);\n        // 通过图片的延迟载入，让网页能更快地显示。但是会造成懒加载失效，因为懒加载的脚本执行更早，所以认为所有未显示的图片都在同一屏幕内，要开始加载。\n//        webSettings.setBlockNetworkImage(true);\n\n//        webSettings.setSupportMultipleWindows(true);\n//        webSettings.setGeolocationEnabled(true);\n//        webSettings.setAppCacheMaxSize(Long.MAX_VALUE);\n//        webSettings.setPageCacheCapacity(IX5WebSettings.DEFAULT_CACHE_CAPACITY);\n//        webSettings.setPluginState(WebSettings.PluginState.ON_DEMAND);\n        // this.getSettingsExtension().setPageCacheCapacity(IX5WebSettings.DEFAULT_CACHE_CAPACITY);//extension\n//        this.evaluateJavascript();//  Android 4.4之后使用evaluateJavascript调用有返回值的JS方法\n    }\n\n\n    @Override\n    public void destroy() {\n        // 链接：http://www.jianshu.com/p/3e8f7dbb0dc7\n        // 如果先调用destroy()方法，则会命中if (isDestroyed()) return;这一行代码，需要先onDetachedFromWindow()，再 destory()。\n        // 在关闭了Activity时，如果Webview的音乐或视频，还在播放。就必须销毁Webview。\n        // 但注意：webview调用destory时，仍绑定在Activity上，这是由于webview构建时传入了Activity对象。\n        // 因此需要先从父容器中移除webview，然后再销毁webview。\n\n//        作者：听话哥\n//        链接：https://www.jianshu.com/p/9293505c7f71\n//        如果你在调用webview.destory();的时候，如果webview里面还有别的线程在操作，就会导致当前这个webview为空。这时候我们需要结束相应线程。例如我们项目中有一个广告拦截是通过在\n//        public void onPageFinished(final WebView view, String url)\n//        里面启用一个runnable去执行一串的js脚本，如果用户在你脚本没执行完成的时候就关闭了当前界面，系统就会抛出空指针异常。这时候就需要通过去onPageFinished获取webview对象\n\n//        setVisibility(View.GONE);\n        try {\n            stopLoading();\n            onPause();\n            CookieManager.getInstance().flush();\n\n            // 退出时调用此方法，移除绑定的服务，否则某些特定系统会报错\n            getSettings().setJavaScriptEnabled(false);\n//            clearFormData(); // 清除自动完成填充的表单数据。需要注意的是，该方法仅仅清除当前表单域自动完成填充的表单数据，并不会清除WebView存储到本地的数据。\n            clearMatches(); // 清除网页查找的高亮匹配字符。\n            clearSslPreferences(); //清除ssl信息。\n            clearDisappearingChildren();\n            clearAnimation();\n\n            clearHistory();\n//            clearCache(true);\n//            destroyDrawingCache(); // 貌似影响内存回收\n            removeAllViews();\n            // removeAllViewsInLayout(); 相比而言, removeAllViews() 也调用了removeAllViewsInLayout(), 但是后面还调用了requestLayout(),这个方法是当View的布局发生改变会调用它来更新当前视图, 移除子View会更加彻底. 所以除非必要, 还是推荐使用removeAllViews()这个方法.\n            ViewGroup parent = (ViewGroup) this.getParent();\n            if (parent != null) {\n                parent.removeView(this);\n            }\n            setWebChromeClient(null);\n            // 将缓存中的cookie数据同步到数据库。此调用将阻塞调用者，直到它完成并可能执行I/O。\n        } catch (Exception e) {\n            KLog.e(\"报错\");\n            e.printStackTrace();\n        }\n        super.destroy();\n    }\n\n\n    /**\n     * 不要将 base url 设为 null\n     * 4.4以上版本，使用evaluateJavascript方法调js方法，会有返回值\n     */\n    public void loadData(String htmlContent) {\n//        stopLoading();\n//        getSettings().setBlockNetworkImage(true); // 将图片下载阻塞\n        loadDataWithBaseURL(App.i().getWebViewBaseUrl(), htmlContent, \"text/html\", \"UTF-8\", null);\n        isReadability = false;\n    }\n\n\n    @Override\n    protected void onScrollChanged(final int l, final int t, final int oldl,\n                                   final int oldt) {\n        super.onScrollChanged(l, t, oldl, oldt);\n        if (mOnScrollChangedCallback != null) {\n            mOnScrollChangedCallback.onScrollY(t - oldt);\n        }\n    }\n\n    public OnScrollChangedCallback getOnScrollChangedCallback() {\n        return mOnScrollChangedCallback;\n    }\n\n    public void setOnScrollChangedCallback(\n            final OnScrollChangedCallback onScrollChangedCallback) {\n        mOnScrollChangedCallback = onScrollChangedCallback;\n    }\n\n    private OnScrollChangedCallback mOnScrollChangedCallback;\n\n    /**\n     * Impliment in the activity/fragment/view that you want to listen to the webview\n     */\n    public interface OnScrollChangedCallback {\n        void onScrollY(int dy);\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/colorful/Colorful.java",
    "content": "package me.wizos.loread.view.colorful;\n\nimport android.app.Activity;\nimport android.content.res.Resources.Theme;\nimport android.util.TypedValue;\nimport android.view.View;\nimport android.widget.TextView;\n\nimport androidx.fragment.app.Fragment;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport me.wizos.loread.R;\nimport me.wizos.loread.view.colorful.setter.TextColorSetter;\nimport me.wizos.loread.view.colorful.setter.ViewBackgroundColorSetter;\nimport me.wizos.loread.view.colorful.setter.ViewBackgroundDrawableSetter;\nimport me.wizos.loread.view.colorful.setter.ViewSetter;\n\n/**\n * 主题切换控制类\n *\n * @author mrsimple\n */\npublic final class Colorful {\n    /**\n     * Colorful Builder\n     */\n    Builder mBuilder;\n\n    /**\n     * private constructor\n     *\n     * @param builder\n     */\n    private Colorful(Builder builder) {\n        mBuilder = builder;\n    }\n\n    /**\n     * 设置新的主题\n     *\n     * @param newTheme\n     */\n    public void setTheme(int newTheme) {\n        mBuilder.setTheme(newTheme);\n    }\n\n    /**\n     * 构建Colorful的Builder对象\n     *\n     * @author mrsimple\n     */\n    public static class Builder {\n        /**\n         * 存储了视图和属性资源id的关系表\n         */\n        Set<ViewSetter> mElements = new HashSet<ViewSetter>();\n        /**\n         * 目标Activity\n         */\n        Activity mActivity;\n\n        /**\n         * @param activity\n         */\n        public Builder(Activity activity) {\n            mActivity = activity;\n        }\n\n        /**\n         * @param fragment\n         */\n        public Builder(Fragment fragment) {\n            mActivity = fragment.getActivity();\n        }\n\n        private View findViewById(int viewId) {\n            return mActivity.findViewById(viewId);\n        }\n\n        /**\n         * 将View id与存储该view背景色的属性进行绑定\n         *\n         * @param viewId  控件id\n         * @param colorId 颜色属性id\n         * @return\n         */\n        public Builder backgroundColor(int viewId, int colorId) {\n            mElements.add(new ViewBackgroundColorSetter(findViewById(viewId),\n                    colorId));\n            return this;\n        }\n\n        /**\n         * 将View id与存储该view背景Drawable的属性进行绑定\n         *\n         * @param viewId     控件id\n         * @param drawableId Drawable属性id\n         * @return\n         */\n        public Builder backgroundDrawable(int viewId, int drawableId) {\n            mElements.add(new ViewBackgroundDrawableSetter(\n                    findViewById(viewId), drawableId));\n            return this;\n        }\n\n        /**\n         * 将TextView id与存储该TextView文本颜色的属性进行绑定\n         *\n         * @param viewId  TextView或者TextView子类控件的id\n         * @param colorId 颜色属性id\n         * @return\n         */\n        public Builder textColor(int viewId, int colorId) {\n            TextView textView = (TextView) findViewById(viewId);\n            mElements.add(new TextColorSetter(textView, colorId));\n            return this;\n        }\n\n        // mElements 用于保存 要修改的 Views\n\n        /**\n         * 用户手动构造并且添加Setter(对于listView来说，只是把旗下某个项的 viewid 和 要改的值 挂钩在一起)\n         *\n         * @param setter 用户自定义的Setter\n         * @return\n         */\n        public Builder setter(ViewSetter setter) {\n            mElements.add(setter);\n            return this;\n        }\n\n        /**\n         * 设置新的主题\n         *\n         * @param newTheme\n         */\n        protected void setTheme(int newTheme) {\n            mActivity.setTheme(newTheme);\n            makeChange(newTheme);\n            refreshStatusBar();\n        }\n\n        /**\n         * 修改各个视图绑定的属性\n         */\n        private void makeChange(int themeId) {\n            Theme curTheme = mActivity.getTheme();\n            for (ViewSetter setter : mElements) {\n                setter.setValue(curTheme, themeId);\n            }\n        }\n\n        /**\n         * 我新加的 方法一\n         * 刷新 StatusBar\n         */\n        private void refreshStatusBar() {\n            TypedValue typedValue = new TypedValue();\n            Theme theme = mActivity.getTheme();\n            theme.resolveAttribute(R.attr.status_bar, typedValue, true);\n            StatusBarUtil.setColorNoTranslucent(mActivity, mActivity.getResources().getColor(typedValue.resourceId));\n//\t\t\tKLog.d(\"【修改状态栏】\" + typedValue.resourceId );\n        }\n\n\n//\t\t/**\n//\t\t * 我新加的 方法二\n//\t\t * 刷新 StatusBar\n//\t\t */\n//\t\tprotected void initSystemBar(int colorId) {\n//\t\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n//\t\t\t\tsetTranslucentStatus(true);\n//\t\t\t\tSystemBarTintManager tintManager = new SystemBarTintManager(mActivity);\n//\t\t\t\ttintManager.setStatusBarTintColor(mActivity.getResources().getColor(R.color.main_grey_dark));\n//\t\t\t\ttintManager.setStatusBarTintEnabled(true);\n//\t\t\t}\n//\t\t}\n//\t\tprivate void setTranslucentStatus(boolean on) {\n//\t\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n//\t\t\t\tWindow win = mActivity.getWindow();\n//\t\t\t\tWindowManager.LayoutParams winParams = win.getAttributes();\n//\t\t\t\tfinal int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;\n//\t\t\t\tif (on) {\n//\t\t\t\t\twinParams.flags |= bits;\n//\t\t\t\t} else {\n//\t\t\t\t\twinParams.flags &= ~bits;\n//\t\t\t\t}\n//\t\t\t\twin.setAttributes(winParams);\n//\t\t\t}\n//\t\t}\n//\n//\n//\t\t/**\n//\t\t * 我新加的 方法三\n//\t\t * 刷新 StatusBar\n//\t\t * 变紫色\n//\t\t */\n//\t\t/**\n//\t\t * 设置状态栏颜色\n//\t\t *\n//\t\t * @param activity 需要设置的activity\n//\t\t * @param color    状态栏颜色值\n//\t\t */\n//\t\tprivate static void setStatusBarColor(Activity activity, int color) {\n//\t\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n//\t\t\t\t// 设置状态栏透明\n//\t\t\t\tactivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);\n////\t\t\t\t 生成一个状态栏大小的矩形\n//\t\t\t\tView statusView = createStatusView(activity, color);\n//\t\t\t\t// 添加 statusView 到布局中\n//\t\t\t\tViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();\n//\t\t\t\tdecorView.addView(statusView);\n//\t\t\t\t// 设置根布局的参数\n//\t\t\t\tViewGroup rootView = (ViewGroup) ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0);\n//\t\t\t\trootView.setFitsSystemWindows(true);\n//\t\t\t\trootView.setClipToPadding(true);\n//\t\t\t}\n//\t\t}\n//\t\t/**\n//\t\t * 生成一个和状态栏大小相同的矩形条\n//\t\t *\n//\t\t * @param activity 需要设置的activity\n//\t\t * @param color    状态栏颜色值\n//\t\t * @return 状态栏矩形条\n//\t\t */\n//\t\tprivate static View createStatusView(Activity activity, int color) {\n//\t\t\t// 获得状态栏高度\n//\t\t\tint resourceId = activity.getResources().getIdentifier(\"status_bar_height\", \"dimen\", \"android\");\n//\t\t\tint statusBarHeight = activity.getResources().getDimensionPixelSize(resourceId);\n//\n//\t\t\t// 绘制一个和状态栏一样高的矩形\n//\t\t\tView statusView = new View(activity);\n//\t\t\tLinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,\n//\t\t\t\t\tstatusBarHeight);\n//\t\t\tstatusView.setLayoutParams(params);\n//\t\t\tstatusView.setBackgroundColor(color);\n//\t\t\treturn statusView;\n//\t\t}\n\n\n        /**\n         * 创建Colorful对象\n         *\n         * @return\n         */\n        public Colorful create() {\n            return new Colorful(this);\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/colorful/StatusBarUtil.java",
    "content": "package me.wizos.loread.view.colorful;\n\nimport android.annotation.TargetApi;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.os.Build;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.WindowManager;\nimport android.widget.LinearLayout;\n\nimport androidx.annotation.ColorInt;\nimport androidx.drawerlayout.widget.DrawerLayout;\n\n/**\n * Created by Jaeger on 16/2/14.\n * <p>\n * Email: chjie.jaeger@gmail.com\n * GitHub: https://github.com/laobie\n */\npublic class StatusBarUtil {\n\n    public static final int DEFAULT_STATUS_BAR_ALPHA = 112;\n\n    /**\n     * 设置状态栏颜色\n     *\n     * @param activity 需要设置的 activity\n     * @param color    状态栏颜色值\n     */\n    public static void setColor(Activity activity, @ColorInt int color) {\n        setColor(activity, color, DEFAULT_STATUS_BAR_ALPHA);\n    }\n\n    /**\n     * 设置状态栏颜色\n     *\n     * @param activity       需要设置的activity\n     * @param color          状态栏颜色值\n     * @param statusBarAlpha 状态栏透明度\n     */\n\n    public static void setColor(Activity activity, @ColorInt int color, int statusBarAlpha) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);\n            activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);\n            activity.getWindow().setStatusBarColor(calculateStatusColor(color, statusBarAlpha));\n        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);\n            ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();\n            int count = decorView.getChildCount();\n            if (count > 0 && decorView.getChildAt(count - 1) instanceof StatusBarView) {\n                decorView.getChildAt(count - 1).setBackgroundColor(calculateStatusColor(color, statusBarAlpha));\n            } else {\n                StatusBarView statusView = createStatusBarView(activity, color, statusBarAlpha);\n                decorView.addView(statusView);\n            }\n            setRootView(activity);\n        }\n    }\n\n    /**\n     * 为滑动返回界面设置状态栏颜色\n     *\n     * @param activity 需要设置的activity\n     * @param color    状态栏颜色值\n     */\n    public static void setColorForSwipeBack(Activity activity, int color) {\n        setColorForSwipeBack(activity, color, DEFAULT_STATUS_BAR_ALPHA);\n    }\n\n    /**\n     * 为滑动返回界面设置状态栏颜色\n     *\n     * @param activity       需要设置的activity\n     * @param color          状态栏颜色值\n     * @param statusBarAlpha 状态栏透明度\n     */\n    public static void setColorForSwipeBack(Activity activity, @ColorInt int color, int statusBarAlpha) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n            ViewGroup contentView = activity.findViewById(android.R.id.content);\n            contentView.setPadding(0, getStatusBarHeight(activity), 0, 0);\n            contentView.setBackgroundColor(calculateStatusColor(color, statusBarAlpha));\n            setTransparentForWindow(activity);\n        }\n    }\n\n    /**\n     * 设置状态栏纯色 不加半透明效果\n     *\n     * @param activity 需要设置的 activity\n     * @param color    状态栏颜色值\n     */\n    public static void setColorNoTranslucent(Activity activity, @ColorInt int color) {\n        setColor(activity, color, 0);\n    }\n\n    /**\n     * 设置状态栏颜色(5.0以下无半透明效果,不建议使用)\n     *\n     * @param activity 需要设置的 activity\n     * @param color    状态栏颜色值\n     */\n    @Deprecated\n    public static void setColorDiff(Activity activity, @ColorInt int color) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {\n            return;\n        }\n        transparentStatusBar(activity);\n        ViewGroup contentView = activity.findViewById(android.R.id.content);\n        // 移除半透明矩形,以免叠加\n        if (contentView.getChildCount() > 1) {\n            contentView.getChildAt(1).setBackgroundColor(color);\n        } else {\n            contentView.addView(createStatusBarView(activity, color));\n        }\n        setRootView(activity);\n    }\n\n    /**\n     * 使状态栏半透明\n     * <p>\n     * 适用于图片作为背景的界面,此时需要图片填充到状态栏\n     *\n     * @param activity 需要设置的activity\n     */\n    public static void setTranslucent(Activity activity) {\n        setTranslucent(activity, DEFAULT_STATUS_BAR_ALPHA);\n    }\n\n    /**\n     * 使状态栏半透明\n     * <p>\n     * 适用于图片作为背景的界面,此时需要图片填充到状态栏\n     *\n     * @param activity       需要设置的activity\n     * @param statusBarAlpha 状态栏透明度\n     */\n    public static void setTranslucent(Activity activity, int statusBarAlpha) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {\n            return;\n        }\n        setTransparent(activity);\n        addTranslucentView(activity, statusBarAlpha);\n    }\n\n    /**\n     * 针对根布局是 CoordinatorLayout, 使状态栏半透明\n     * <p>\n     * 适用于图片作为背景的界面,此时需要图片填充到状态栏\n     *\n     * @param activity       需要设置的activity\n     * @param statusBarAlpha 状态栏透明度\n     */\n    public static void setTranslucentForCoordinatorLayout(Activity activity, int statusBarAlpha) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {\n            return;\n        }\n        transparentStatusBar(activity);\n        addTranslucentView(activity, statusBarAlpha);\n    }\n\n    /**\n     * 设置状态栏全透明\n     *\n     * @param activity 需要设置的activity\n     */\n    public static void setTransparent(Activity activity) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {\n            return;\n        }\n        transparentStatusBar(activity);\n        setRootView(activity);\n    }\n\n    /**\n     * 使状态栏透明(5.0以上半透明效果,不建议使用)\n     * <p>\n     * 适用于图片作为背景的界面,此时需要图片填充到状态栏\n     *\n     * @param activity 需要设置的activity\n     */\n    @Deprecated\n    public static void setTranslucentDiff(Activity activity) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n            // 设置状态栏透明\n            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);\n            setRootView(activity);\n        }\n    }\n\n    /**\n     * 为DrawerLayout 布局设置状态栏变色\n     *\n     * @param activity     需要设置的activity\n     * @param drawerLayout DrawerLayout\n     * @param color        状态栏颜色值\n     */\n    public static void setColorForDrawerLayout(Activity activity, DrawerLayout drawerLayout, @ColorInt int color) {\n        setColorForDrawerLayout(activity, drawerLayout, color, DEFAULT_STATUS_BAR_ALPHA);\n    }\n\n    /**\n     * 为DrawerLayout 布局设置状态栏颜色,纯色\n     *\n     * @param activity     需要设置的activity\n     * @param drawerLayout DrawerLayout\n     * @param color        状态栏颜色值\n     */\n    public static void setColorNoTranslucentForDrawerLayout(Activity activity, DrawerLayout drawerLayout, @ColorInt int color) {\n        setColorForDrawerLayout(activity, drawerLayout, color, 0);\n    }\n\n    /**\n     * 为DrawerLayout 布局设置状态栏变色\n     *\n     * @param activity       需要设置的activity\n     * @param drawerLayout   DrawerLayout\n     * @param color          状态栏颜色值\n     * @param statusBarAlpha 状态栏透明度\n     */\n    public static void setColorForDrawerLayout(Activity activity, DrawerLayout drawerLayout, @ColorInt int color,\n                                               int statusBarAlpha) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {\n            return;\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);\n            activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);\n            activity.getWindow().setStatusBarColor(Color.TRANSPARENT);\n        } else {\n            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);\n        }\n        // 生成一个状态栏大小的矩形\n        // 添加 statusBarView 到布局中\n        ViewGroup contentLayout = (ViewGroup) drawerLayout.getChildAt(0);\n        if (contentLayout.getChildCount() > 0 && contentLayout.getChildAt(0) instanceof StatusBarView) {\n            contentLayout.getChildAt(0).setBackgroundColor(calculateStatusColor(color, statusBarAlpha));\n        } else {\n            StatusBarView statusBarView = createStatusBarView(activity, color);\n            contentLayout.addView(statusBarView, 0);\n        }\n        // 内容布局不是 LinearLayout 时,设置padding top\n        if (!(contentLayout instanceof LinearLayout) && contentLayout.getChildAt(1) != null) {\n            contentLayout.getChildAt(1)\n                    .setPadding(contentLayout.getPaddingLeft(), getStatusBarHeight(activity) + contentLayout.getPaddingTop(),\n                            contentLayout.getPaddingRight(), contentLayout.getPaddingBottom());\n        }\n        // 设置属性\n        setDrawerLayoutProperty(drawerLayout, contentLayout);\n        addTranslucentView(activity, statusBarAlpha);\n    }\n\n    /**\n     * 设置 DrawerLayout 属性\n     *\n     * @param drawerLayout              DrawerLayout\n     * @param drawerLayoutContentLayout DrawerLayout 的内容布局\n     */\n    private static void setDrawerLayoutProperty(DrawerLayout drawerLayout, ViewGroup drawerLayoutContentLayout) {\n        ViewGroup drawer = (ViewGroup) drawerLayout.getChildAt(1);\n        drawerLayout.setFitsSystemWindows(false);\n        drawerLayoutContentLayout.setFitsSystemWindows(false);\n        drawerLayoutContentLayout.setClipToPadding(true);\n        drawer.setFitsSystemWindows(false);\n    }\n\n    /**\n     * 为DrawerLayout 布局设置状态栏变色(5.0以下无半透明效果,不建议使用)\n     *\n     * @param activity     需要设置的activity\n     * @param drawerLayout DrawerLayout\n     * @param color        状态栏颜色值\n     */\n    @Deprecated\n    public static void setColorForDrawerLayoutDiff(Activity activity, DrawerLayout drawerLayout, @ColorInt int color) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);\n            // 生成一个状态栏大小的矩形\n            ViewGroup contentLayout = (ViewGroup) drawerLayout.getChildAt(0);\n            if (contentLayout.getChildCount() > 0 && contentLayout.getChildAt(0) instanceof StatusBarView) {\n                contentLayout.getChildAt(0).setBackgroundColor(calculateStatusColor(color, DEFAULT_STATUS_BAR_ALPHA));\n            } else {\n                // 添加 statusBarView 到布局中\n                StatusBarView statusBarView = createStatusBarView(activity, color);\n                contentLayout.addView(statusBarView, 0);\n            }\n            // 内容布局不是 LinearLayout 时,设置padding top\n            if (!(contentLayout instanceof LinearLayout) && contentLayout.getChildAt(1) != null) {\n                contentLayout.getChildAt(1).setPadding(0, getStatusBarHeight(activity), 0, 0);\n            }\n            // 设置属性\n            setDrawerLayoutProperty(drawerLayout, contentLayout);\n        }\n    }\n\n    /**\n     * 为 DrawerLayout 布局设置状态栏透明\n     *\n     * @param activity     需要设置的activity\n     * @param drawerLayout DrawerLayout\n     */\n    public static void setTranslucentForDrawerLayout(Activity activity, DrawerLayout drawerLayout) {\n        setTranslucentForDrawerLayout(activity, drawerLayout, DEFAULT_STATUS_BAR_ALPHA);\n    }\n\n    /**\n     * 为 DrawerLayout 布局设置状态栏透明\n     *\n     * @param activity     需要设置的activity\n     * @param drawerLayout DrawerLayout\n     */\n    public static void setTranslucentForDrawerLayout(Activity activity, DrawerLayout drawerLayout, int statusBarAlpha) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {\n            return;\n        }\n        setTransparentForDrawerLayout(activity, drawerLayout);\n        addTranslucentView(activity, statusBarAlpha);\n    }\n\n    /**\n     * 为 DrawerLayout 布局设置状态栏透明\n     *\n     * @param activity     需要设置的activity\n     * @param drawerLayout DrawerLayout\n     */\n    public static void setTransparentForDrawerLayout(Activity activity, DrawerLayout drawerLayout) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {\n            return;\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);\n            activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);\n            activity.getWindow().setStatusBarColor(Color.TRANSPARENT);\n        } else {\n            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);\n        }\n\n        ViewGroup contentLayout = (ViewGroup) drawerLayout.getChildAt(0);\n        // 内容布局不是 LinearLayout 时,设置padding top\n        if (!(contentLayout instanceof LinearLayout) && contentLayout.getChildAt(1) != null) {\n            contentLayout.getChildAt(1).setPadding(0, getStatusBarHeight(activity), 0, 0);\n        }\n\n        // 设置属性\n        setDrawerLayoutProperty(drawerLayout, contentLayout);\n    }\n\n    /**\n     * 为 DrawerLayout 布局设置状态栏透明(5.0以上半透明效果,不建议使用)\n     *\n     * @param activity     需要设置的activity\n     * @param drawerLayout DrawerLayout\n     */\n    @Deprecated\n    public static void setTranslucentForDrawerLayoutDiff(Activity activity, DrawerLayout drawerLayout) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n            // 设置状态栏透明\n            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);\n            // 设置内容布局属性\n            ViewGroup contentLayout = (ViewGroup) drawerLayout.getChildAt(0);\n            contentLayout.setFitsSystemWindows(true);\n            contentLayout.setClipToPadding(true);\n            // 设置抽屉布局属性\n            ViewGroup vg = (ViewGroup) drawerLayout.getChildAt(1);\n            vg.setFitsSystemWindows(false);\n            // 设置 DrawerLayout 属性\n            drawerLayout.setFitsSystemWindows(false);\n        }\n    }\n\n    /**\n     * 为头部是 ImageView 的界面设置状态栏全透明\n     *\n     * @param activity       需要设置的activity\n     * @param needOffsetView 需要向下偏移的 View\n     */\n    public static void setTransparentForImageView(Activity activity, View needOffsetView) {\n        setTranslucentForImageView(activity, 0, needOffsetView);\n    }\n\n    /**\n     * 为头部是 ImageView 的界面设置状态栏透明(使用默认透明度)\n     *\n     * @param activity       需要设置的activity\n     * @param needOffsetView 需要向下偏移的 View\n     */\n    public static void setTranslucentForImageView(Activity activity, View needOffsetView) {\n        setTranslucentForImageView(activity, DEFAULT_STATUS_BAR_ALPHA, needOffsetView);\n    }\n\n    /**\n     * 为头部是 ImageView 的界面设置状态栏透明\n     *\n     * @param activity       需要设置的activity\n     * @param statusBarAlpha 状态栏透明度\n     * @param needOffsetView 需要向下偏移的 View\n     */\n    public static void setTranslucentForImageView(Activity activity, int statusBarAlpha, View needOffsetView) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {\n            return;\n        }\n        setTransparentForWindow(activity);\n        addTranslucentView(activity, statusBarAlpha);\n        if (needOffsetView != null) {\n            ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) needOffsetView.getLayoutParams();\n            layoutParams.setMargins(0, getStatusBarHeight(activity), 0, 0);\n        }\n    }\n\n    /**\n     * 为 fragment 头部是 ImageView 的设置状态栏透明\n     *\n     * @param activity       fragment 对应的 activity\n     * @param needOffsetView 需要向下偏移的 View\n     */\n    public static void setTranslucentForImageViewInFragment(Activity activity, View needOffsetView) {\n        setTranslucentForImageViewInFragment(activity, DEFAULT_STATUS_BAR_ALPHA, needOffsetView);\n    }\n\n    /**\n     * 为 fragment 头部是 ImageView 的设置状态栏透明\n     *\n     * @param activity       fragment 对应的 activity\n     * @param needOffsetView 需要向下偏移的 View\n     */\n    public static void setTransparentForImageViewInFragment(Activity activity, View needOffsetView) {\n        setTranslucentForImageViewInFragment(activity, 0, needOffsetView);\n    }\n\n    /**\n     * 为 fragment 头部是 ImageView 的设置状态栏透明\n     *\n     * @param activity       fragment 对应的 activity\n     * @param statusBarAlpha 状态栏透明度\n     * @param needOffsetView 需要向下偏移的 View\n     */\n    public static void setTranslucentForImageViewInFragment(Activity activity, int statusBarAlpha, View needOffsetView) {\n        setTranslucentForImageView(activity, statusBarAlpha, needOffsetView);\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {\n            clearPreviousSetting(activity);\n        }\n    }\n\n    ///////////////////////////////////////////////////////////////////////////////////\n\n    @TargetApi(Build.VERSION_CODES.KITKAT)\n    private static void clearPreviousSetting(Activity activity) {\n        ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();\n        int count = decorView.getChildCount();\n        if (count > 0 && decorView.getChildAt(count - 1) instanceof StatusBarView) {\n            decorView.removeViewAt(count - 1);\n            ViewGroup rootView = (ViewGroup) ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0);\n            rootView.setPadding(0, 0, 0, 0);\n        }\n    }\n\n    /**\n     * 添加半透明矩形条\n     *\n     * @param activity       需要设置的 activity\n     * @param statusBarAlpha 透明值\n     */\n    private static void addTranslucentView(Activity activity, int statusBarAlpha) {\n        ViewGroup contentView = activity.findViewById(android.R.id.content);\n        if (contentView.getChildCount() > 1) {\n            contentView.getChildAt(1).setBackgroundColor(Color.argb(statusBarAlpha, 0, 0, 0));\n        } else {\n            contentView.addView(createTranslucentStatusBarView(activity, statusBarAlpha));\n        }\n    }\n\n    /**\n     * 生成一个和状态栏大小相同的彩色矩形条\n     *\n     * @param activity 需要设置的 activity\n     * @param color    状态栏颜色值\n     * @return 状态栏矩形条\n     */\n    private static StatusBarView createStatusBarView(Activity activity, @ColorInt int color) {\n        // 绘制一个和状态栏一样高的矩形\n        StatusBarView statusBarView = new StatusBarView(activity);\n        LinearLayout.LayoutParams params =\n                new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight(activity));\n        statusBarView.setLayoutParams(params);\n        statusBarView.setBackgroundColor(color);\n        return statusBarView;\n    }\n\n    /**\n     * 生成一个和状态栏大小相同的半透明矩形条\n     *\n     * @param activity 需要设置的activity\n     * @param color    状态栏颜色值\n     * @param alpha    透明值\n     * @return 状态栏矩形条\n     */\n    private static StatusBarView createStatusBarView(Activity activity, @ColorInt int color, int alpha) {\n        // 绘制一个和状态栏一样高的矩形\n        StatusBarView statusBarView = new StatusBarView(activity);\n        LinearLayout.LayoutParams params =\n                new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight(activity));\n        statusBarView.setLayoutParams(params);\n        statusBarView.setBackgroundColor(calculateStatusColor(color, alpha));\n        return statusBarView;\n    }\n\n    /**\n     * 设置根布局参数\n     */\n    private static void setRootView(Activity activity) {\n        ViewGroup parent = activity.findViewById(android.R.id.content);\n        for (int i = 0, count = parent.getChildCount(); i < count; i++) {\n            View childView = parent.getChildAt(i);\n            if (childView instanceof ViewGroup) {\n                childView.setFitsSystemWindows(true);\n                ((ViewGroup) childView).setClipToPadding(true);\n            }\n        }\n    }\n\n    /**\n     * 设置透明\n     */\n    private static void setTransparentForWindow(Activity activity) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            activity.getWindow().setStatusBarColor(Color.TRANSPARENT);\n            activity.getWindow()\n                    .getDecorView()\n                    .setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);\n        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n            activity.getWindow()\n                    .setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);\n        }\n    }\n\n    /**\n     * 使状态栏透明\n     */\n    @TargetApi(Build.VERSION_CODES.KITKAT)\n    private static void transparentStatusBar(Activity activity) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);\n            activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);\n            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);\n            activity.getWindow().setStatusBarColor(Color.TRANSPARENT);\n        } else {\n            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);\n        }\n    }\n\n    /**\n     * 创建半透明矩形 View\n     *\n     * @param alpha 透明值\n     * @return 半透明 View\n     */\n    private static StatusBarView createTranslucentStatusBarView(Activity activity, int alpha) {\n        // 绘制一个和状态栏一样高的矩形\n        StatusBarView statusBarView = new StatusBarView(activity);\n        LinearLayout.LayoutParams params =\n                new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight(activity));\n        statusBarView.setLayoutParams(params);\n        statusBarView.setBackgroundColor(Color.argb(alpha, 0, 0, 0));\n        return statusBarView;\n    }\n\n    /**\n     * 获取状态栏高度\n     *\n     * @param context context\n     * @return 状态栏高度\n     */\n    private static int getStatusBarHeight(Context context) {\n        // 获得状态栏高度\n        int resourceId = context.getResources().getIdentifier(\"status_bar_height\", \"dimen\", \"android\");\n        return context.getResources().getDimensionPixelSize(resourceId);\n    }\n\n    /**\n     * 计算状态栏颜色\n     *\n     * @param color color值\n     * @param alpha alpha值\n     * @return 最终的状态栏颜色\n     */\n    private static int calculateStatusColor(@ColorInt int color, int alpha) {\n        float a = 1 - alpha / 255f;\n        int red = color >> 16 & 0xff;\n        int green = color >> 8 & 0xff;\n        int blue = color & 0xff;\n        red = (int) (red * a + 0.5);\n        green = (int) (green * a + 0.5);\n        blue = (int) (blue * a + 0.5);\n        return 0xff << 24 | red << 16 | green << 8 | blue;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/colorful/StatusBarView.java",
    "content": "package me.wizos.loread.view.colorful;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.View;\n\n/**\n * Created by Jaeger on 16/6/8.\n * <p>\n * Email: chjie.jaeger@gmail.com\n * GitHub: https://github.com/laobie\n */\npublic class StatusBarView extends View {\n    public StatusBarView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public StatusBarView(Context context) {\n        super(context);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/colorful/setter/TextColorSetter.java",
    "content": "package me.wizos.loread.view.colorful.setter;\n\nimport android.content.res.Resources.Theme;\nimport android.widget.TextView;\n\n/**\n * TextView 文本颜色Setter\n *\n * @author mrsimple\n */\npublic class TextColorSetter extends ViewSetter {\n\n    public TextColorSetter(TextView textView, int resId) {\n        super(textView, resId);\n    }\n\n    public TextColorSetter(int viewId, int resId) {\n        super(viewId, resId);\n    }\n\n    @Override\n    public void setValue(Theme newTheme, int themeId) {\n        if (mView == null) {\n            return;\n        }\n        ((TextView) mView).setTextColor(getColor(newTheme));\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/colorful/setter/ViewBackgroundColorSetter.java",
    "content": "package me.wizos.loread.view.colorful.setter;\n\nimport android.content.res.Resources.Theme;\nimport android.view.View;\n\n/**\n * View的背景色Setter\n *\n * @author mrsimple\n */\npublic class ViewBackgroundColorSetter extends ViewSetter {\n\n    public ViewBackgroundColorSetter(View target, int resId) {\n        super(target, resId);\n    }\n\n    public ViewBackgroundColorSetter(int viewId, int resId) {\n        super(viewId, resId);\n    }\n\n    @Override\n    public void setValue(Theme newTheme, int themeId) {\n        if (mView != null) {\n            int alpha = 255;\n            if (mView.getBackground() != null) {\n                alpha = mView.getBackground().getAlpha();// 自加。保留透明度信息。\n            }\n            mView.setBackgroundColor(getColor(newTheme));\n            mView.getBackground().setAlpha(alpha);// 自加。保留透明度信息。\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/colorful/setter/ViewBackgroundDrawableSetter.java",
    "content": "package me.wizos.loread.view.colorful.setter;\n\nimport android.content.res.Resources.Theme;\nimport android.content.res.TypedArray;\nimport android.graphics.drawable.Drawable;\nimport android.view.View;\n\n/**\n * View的背景Drawabler Setter\n *\n * @author mrsimple\n */\npublic final class ViewBackgroundDrawableSetter extends ViewSetter {\n\n    public ViewBackgroundDrawableSetter(View targetView, int resId) {\n        super(targetView, resId);\n    }\n\n\n    public ViewBackgroundDrawableSetter(int viewId, int resId) {\n        super(viewId, resId);\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    @Override\n    public void setValue(Theme newTheme, int themeId) {\n        if (mView == null) {\n            return;\n        }\n        TypedArray a = newTheme.obtainStyledAttributes(themeId,\n                new int[]{mAttrResId});\n        int attributeResourceId = a.getResourceId(0, 0);\n        Drawable drawable = mView.getResources().getDrawable(\n                attributeResourceId);\n        a.recycle();\n        mView.setBackgroundDrawable(drawable);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/colorful/setter/ViewGroupSetter.java",
    "content": "package me.wizos.loread.view.colorful.setter;\n\nimport android.content.res.Resources.Theme;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.AbsListView;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.util.HashSet;\nimport java.util.Set;\n\n/**\n * ViewGroup类型的Setter,用于修改ListView、RecyclerView等ViewGroup类型的Item\n * View,核心思想为遍历每个Item View中的子控件,然后根据用户绑定的view\n * id与属性来将View修改为当前Theme下的最新属性值，达到ViewGroup控件的换肤效果。\n * <p>\n * TODO : Color与Drawable的设计问题,是否需要修改为桥接模式 {@see ViewBackgroundColorSetter}、\n * {@see ViewBackgroundDrawableSetter}\n *\n * @author mrsimple\n */\npublic class ViewGroupSetter extends ViewSetter {\n\n    /**\n     * ListView的子试图的Setter\n     */\n    protected Set<ViewSetter> mItemViewSetters = new HashSet<ViewSetter>();\n\n    /**\n     * @param targetView\n     * @param resId\n     */\n    public ViewGroupSetter(ViewGroup targetView, int resId) {\n        super(targetView, resId);\n    }\n\n    public ViewGroupSetter(ViewGroup targetView) {\n        super(targetView, 0);\n    }\n\n    /**\n     * 设置View的背景色\n     *\n     * @param viewId\n     * @param colorId\n     * @return\n     */\n    public ViewGroupSetter childViewBgColor(int viewId, int colorId) {\n        mItemViewSetters.add(new ViewBackgroundColorSetter(viewId, colorId));\n        return this;\n    }\n\n    /**\n     * 设置View的drawable背景\n     *\n     * @param viewId\n     * @param drawableId\n     * @return\n     */\n    public ViewGroupSetter childViewBgDrawable(int viewId, int drawableId) {\n        mItemViewSetters.add(new ViewBackgroundDrawableSetter(viewId, drawableId));\n        return this;\n    }\n\n    /**\n     * 设置文本颜色,因此View的类型必须为TextView或者其子类\n     *\n     * @param viewId\n     * @param colorId\n     * @return\n     */\n    public ViewGroupSetter childViewTextColor(int viewId, int colorId) {\n        mItemViewSetters.add(new TextColorSetter(viewId, colorId));\n        return this;\n    }\n\n    @Override\n    public void setValue(Theme newTheme, int themeId) {\n        int alpha = 255;\n        if (mView == null) {\n            return;\n        }\n        if (mView.getBackground() != null) {\n            alpha = mView.getBackground().getAlpha();// 自加。保留透明度信息。\n        }\n        mView.setBackgroundColor(getColor(newTheme));\n        mView.getBackground().setAlpha(alpha);// 自加。保留透明度信息。\n        // 清空AbsListView的元素\n        clearListViewRecyclerBin(mView);\n        // 清空RecyclerView\n        clearRecyclerViewRecyclerBin(mView);\n        // 修改所有子元素的相关属性\n        changeChildenAttrs((ViewGroup) mView, newTheme, themeId);\n    }\n\n    /**\n     * @param viewId\n     * @return\n     */\n    private View findViewById(View rootView, int viewId) {\n//\t\tLog.d(\"\", \"### viewgroup find view : \" + targetView);\n        return rootView.findViewById(viewId);\n    }\n\n    /**\n     * 修改子视图的对应属性\n     *\n     * @param viewGroup\n     * @param newTheme\n     * @param themeId\n     */\n    private void changeChildenAttrs(ViewGroup viewGroup, Theme newTheme, int themeId) {\n        int childCount = viewGroup.getChildCount();\n        for (int i = 0; i < childCount; i++) {\n            View childView = viewGroup.getChildAt(i);\n            // 深度遍历\n            if (childView instanceof ViewGroup) {\n                changeChildenAttrs((ViewGroup) childView, newTheme, themeId);\n            }\n\n            // 遍历子元素与要修改的属性,如果相同那么则修改子View的属性\n            for (ViewSetter setter : mItemViewSetters) {\n                // 每次都要从ViewGroup中查找数据\n                setter.mView = findViewById(viewGroup, setter.mViewId);\n\n//\t\t\t\tLog.e(\"\", \"### childView : \" + childView + \", id = \"\n//\t\t\t\t\t\t+ childView.getId());\n//\t\t\t\tLog.e(\"\", \"### setter view : \" + setter.mView + \", id = \"\n//\t\t\t\t\t\t+ setter.getViewId());\n                if (childView.getId() == setter.getViewId()) {\n                    setter.setValue(newTheme, themeId);\n//\t\t\t\t\tLog.e(\"\", \"@@@ 修改新的属性: \" + childView);\n                }\n            }\n        }\n    }\n\n    private void clearListViewRecyclerBin(View rootView) {\n        if (rootView instanceof AbsListView) {\n            try {\n                Field localField = AbsListView.class\n                        .getDeclaredField(\"mRecycler\");\n                localField.setAccessible(true);\n                Method localMethod = Class.forName(\n                        \"android.widget.AbsListView$RecycleBin\")\n                        .getDeclaredMethod(\"clear\");\n                localMethod.setAccessible(true);\n                localMethod.invoke(localField.get(rootView));\n//\t\t\t\tLog.e(\"\", \"### 清空AbsListView的RecycerBin \");\n            } catch (NoSuchFieldException e1) {\n                e1.printStackTrace();\n            } catch (ClassNotFoundException e2) {\n                e2.printStackTrace();\n            } catch (NoSuchMethodException e3) {\n                e3.printStackTrace();\n            } catch (IllegalAccessException e4) {\n                e4.printStackTrace();\n            } catch (InvocationTargetException e5) {\n                e5.printStackTrace();\n            }\n        }\n    }\n\n    private void clearRecyclerViewRecyclerBin(View rootView) {\n//        KLog.e(\"\", \"### 准备 清空RecyclerView的Recycer \");\n        if (rootView instanceof RecyclerView) {\n            try {\n                Field localField = RecyclerView.class.getDeclaredField(\"mRecycler\");\n                localField.setAccessible(true);\n                Method localMethod = Class.forName(\n                        \"androidx.recyclerview.widget.RecyclerView$Recycler\")\n                        .getDeclaredMethod(\"clear\", new Class[0]);\n                localMethod.setAccessible(true);\n                localMethod.invoke(localField.get(rootView), new Object[0]);\n                ((RecyclerView) rootView).getRecycledViewPool().clear();\n\n                rootView.invalidate();\n\n                // KLog.e(\"\", \"### 清空RecyclerView的Recycer \");\n            } catch (NoSuchFieldException e1) {\n                e1.printStackTrace();\n            } catch (ClassNotFoundException e2) {\n                e2.printStackTrace();\n            } catch (NoSuchMethodException e3) {\n                e3.printStackTrace();\n            } catch (IllegalAccessException e4) {\n                e4.printStackTrace();\n            } catch (InvocationTargetException e5) {\n                e5.printStackTrace();\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/colorful/setter/ViewSetter.java",
    "content": "package me.wizos.loread.view.colorful.setter;\n\nimport android.content.res.Resources.Theme;\nimport android.util.TypedValue;\nimport android.view.View;\n\n/**\n * ViewSetter，用于通过{@see #mAttrResId}\n * 设置View的某个属性值,例如背景Drawable、背景色、文本颜色等。如需修改其他属性,可以自行扩展ViewSetter.\n *\n * @author mrsimple\n */\npublic abstract class ViewSetter {\n    /**\n     * 目标View\n     */\n    protected View mView;\n    /**\n     * 目标view id,有时在初始化时还未构建该视图,比如ListView的Item View中的某个控件\n     */\n    protected int mViewId;\n    /**\n     * 目标View要的特定属性id\n     */\n    protected int mAttrResId;\n\n    public ViewSetter(View targetView, int resId) {\n        mView = targetView;\n        mAttrResId = resId;\n    }\n\n    public ViewSetter(int viewId, int resId) {\n        mViewId = viewId;\n        mAttrResId = resId;\n    }\n\n    /**\n     * @param newTheme\n     * @param themeId\n     */\n    public abstract void setValue(Theme newTheme, int themeId);\n\n    /**\n     * 获取视图的Id\n     *\n     * @return\n     */\n    protected int getViewId() {\n        return mView != null ? mView.getId() : -1;\n    }\n\n    protected boolean isViewNotFound() {\n        return mView == null;\n    }\n\n    /**\n     * @param newTheme\n     * @return\n     */\n    protected int getColor(Theme newTheme) {\n        TypedValue typedValue = new TypedValue();\n        newTheme.resolveAttribute(mAttrResId, typedValue, true);\n        return typedValue.data;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/fastscroll/FastScrollDelegate.java",
    "content": "package me.wizos.loread.view.fastscroll;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.ColorFilter;\nimport android.graphics.Interpolator;\nimport android.graphics.Paint;\nimport android.graphics.Path;\nimport android.graphics.PixelFormat;\nimport android.graphics.Rect;\nimport android.graphics.RectF;\nimport android.graphics.drawable.Drawable;\nimport android.graphics.drawable.GradientDrawable;\nimport android.graphics.drawable.InsetDrawable;\nimport android.graphics.drawable.StateListDrawable;\nimport android.text.TextUtils.TruncateAt;\nimport android.util.Log;\nimport android.util.TypedValue;\nimport android.view.Gravity;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewConfiguration;\nimport android.view.animation.AnimationUtils;\nimport android.widget.AbsListView;\nimport android.widget.PopupWindow;\nimport android.widget.TextView;\n\nimport androidx.core.view.ViewCompat;\n\n/**\n * 实现 ListView ，WebView 等的快速滚动条\n * https://github.com/Mixiaoxiao/FastScroll-Everywhere FastScrollDelegate\n *\n * @author Mixiaoxiao 2016-08-28\n */\npublic class FastScrollDelegate {\n\n    @SuppressWarnings(\"unused\")\n    private void log(String msg) {\n        Log.d(\"FastScrollDelegate\", msg);\n    }\n\n    public interface FastScrollable {\n\n        void superOnTouchEvent(MotionEvent event);\n\n        int superComputeVerticalScrollExtent();\n\n        int superComputeVerticalScrollOffset();\n\n        int superComputeVerticalScrollRange();\n\n        View getFastScrollableView();\n\n        FastScrollDelegate getFastScrollDelegate();\n\n        void setNewFastScrollDelegate(FastScrollDelegate newDelegate);\n    }\n\n    public interface OnFastScrollListener {\n        void onFastScrollStart(View view, FastScrollDelegate delegate);\n\n        void onFastScrolled(View view, FastScrollDelegate delegate, int touchDeltaY, int viewScrollDeltaY,\n                            float scrollPercent);\n\n        void onFastScrollEnd(View view, FastScrollDelegate delegate);\n    }\n\n    // codes from FastScroller (for AbsListView)\n    /**\n     * Duration of fade-out animation.\n     */\n    public static int FASTSCROLLER_DURATION_FADE_OUT = 300;\n    /**\n     * Duration of fade-in animation.\n     */\n    public static int FASTSCROLLER_DURATION_FADE_IN = 150;\n    /**\n     * Inactivity timeout before fading controls.\n     */\n    public static long FASTSCROLLER_FADE_TIMEOUT = 1500;\n\n    private static final int[] DRAWABLE_STATE_PRESSED = new int[]{android.R.attr.state_pressed};\n    private static final int[] DRAWABLE_STATE_DEFAULT = new int[]{};\n\n    private final View mView;\n    private final float mDensity;\n    private float mDownY;\n    private final Rect mThumbRect;\n    private Drawable mThumbDrawable;\n    private final FastScrollable mFastScrollable;\n    private int mThumbMinHeight;\n    private final ScrollabilityCache mScrollCache;\n    private IndicatorPopup mIndicatorPopup;\n    private boolean mThumbDynamicHeight;\n\n    private OnFastScrollListener mFastScrollListener;\n    private boolean mIsHanlingTouchEvent = false;\n\n    private FastScrollDelegate(final FastScrollable fastScrollable, int width, int height, Drawable thumbDrawable,\n                               boolean isDynamicHeight) {\n        super();\n        this.mView = fastScrollable.getFastScrollableView();\n        mView.setVerticalScrollBarEnabled(false);\n        Context context = mView.getContext();\n        this.mDensity = context.getResources().getDisplayMetrics().density;\n        this.mThumbMinHeight = dp2px(FASTSCROLLER_MIN_HEIGHT_DP);\n        this.mThumbRect = new Rect(0, 0, width, height);\n        this.mThumbDrawable = thumbDrawable;\n        this.mFastScrollable = fastScrollable;\n        this.mScrollCache = new ScrollabilityCache(ViewConfiguration.get(context), mView);\n        this.mThumbDynamicHeight = isDynamicHeight;\n    }\n\n    // ===========================================================\n    // Useful methods\n    // ===========================================================\n    public void setThumbDrawable(Drawable drawable) {\n        if (drawable == null) {\n            throw new IllegalArgumentException(\"setThumbDrawable must NOT be NULL\");\n        }\n        mThumbDrawable = drawable;\n        updateThumbRect(0);\n    }\n\n    public void setThumbSize(int widthDp, int heightDp) {\n        mThumbRect.left = mThumbRect.right - dp2px(widthDp);\n        mThumbMinHeight = dp2px(heightDp);\n        updateThumbRect(0);\n    }\n\n    public void setThumbDynamicHeight(boolean isDynamicHeight) {\n        if (mThumbDynamicHeight != isDynamicHeight) {\n            mThumbDynamicHeight = isDynamicHeight;\n            updateThumbRect(0);\n        }\n    }\n\n    public void setOnFastScrollListener(OnFastScrollListener l) {\n        mFastScrollListener = l;\n    }\n\n    // ===========================================================\n    // Delegate\n    // ===========================================================\n\n    // See View.class\n    public boolean awakenScrollBars() {\n        return awakenScrollBars(FASTSCROLLER_FADE_TIMEOUT);// Cache.scrollBarDefaultDelayBeforeFade\n    }\n\n    // See View.class\n    private boolean initialAwakenScrollBars() {\n        return awakenScrollBars(mScrollCache.scrollBarDefaultDelayBeforeFade * 4);\n    }\n\n    // See View.class\n    public boolean awakenScrollBars(long startDelay) {\n        ViewCompat.postInvalidateOnAnimation(mView);\n        // log(\"awakenScrollBars call startDelay->\" + startDelay);\n        if (!mIsHanlingTouchEvent) {\n            if (mScrollCache.state == ScrollabilityCache.OFF) {\n                // FIXME: this is copied from WindowManagerService.\n                // We should get this value from the system when it\n                // is possible to do so.\n                final int KEY_REPEAT_FIRST_DELAY = 750;\n                startDelay = Math.max(KEY_REPEAT_FIRST_DELAY, startDelay);\n            }\n            // Tell mScrollCache when we should start fading. This may\n            // extend the fade start time if one was already scheduled\n            long fadeStartTime = AnimationUtils.currentAnimationTimeMillis() + startDelay;\n            mScrollCache.fadeStartTime = fadeStartTime;\n            mScrollCache.state = ScrollabilityCache.ON;\n            // Schedule our fader to run, unscheduling any old ones first\n            // if (mAttachInfo != null) {\n            // mAttachInfo.mHandler.removeCallbacks(scrollCache);\n            // mAttachInfo.mHandler.postAtTime(scrollCache, fadeStartTime);\n            // }\n            mView.removeCallbacks(mScrollCache);\n            mView.postDelayed(mScrollCache, fadeStartTime - AnimationUtils.currentAnimationTimeMillis());\n        }\n        return false;\n    }\n\n    // ===========================================================\n    // TouchEvent Delegate\n    // ===========================================================\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n        return onInterceptTouchEventInternal(ev);\n    }\n\n    public boolean onTouchEvent(MotionEvent event) {\n        return onTouchEventInternal(event);\n    }\n\n    // ===========================================================\n    // TouchEvent Internal\n    // ===========================================================\n    private boolean onInterceptTouchEventInternal(MotionEvent ev) {\n        final int action = ev.getActionMasked();\n        if (action == MotionEvent.ACTION_DOWN) {\n            // Just check if hit the thumb\n            return onTouchEventInternal(ev);\n        }\n        return false;\n    }\n\n    private boolean onTouchEventInternal(MotionEvent event) {\n//        KLog.e(\"执行父onTouchEventInternal：\"  );\n        final int action = event.getActionMasked();\n        final float y = event.getY();\n        switch (action) {\n            case MotionEvent.ACTION_DOWN: {\n                // log(\"onTouchEvent ACTION_DOWN\");\n                if (mScrollCache.state == ScrollabilityCache.OFF) {\n                    mIsHanlingTouchEvent = false;\n                    return false;\n                }\n                if (!mIsHanlingTouchEvent) {\n                    updateThumbRect(0);\n                    final float x = event.getX();\n                    // Check if hit the thumb, Rect.contains(int x ,int y) is NOT\n                    // exact\n                    if (y >= mThumbRect.top && y <= mThumbRect.bottom && x >= mThumbRect.left && x <= mThumbRect.right) {\n                        mIsHanlingTouchEvent = true;\n                        mDownY = y;\n                        // try to stop scroll\n                        // step 0: call super ACTION_DOWN\n                        mFastScrollable.superOnTouchEvent(event);\n                        // step 1: call super ACTION_CANCEL\n                        MotionEvent fakeCancelMotionEvent = MotionEvent.obtain(event);\n                        fakeCancelMotionEvent.setAction(MotionEvent.ACTION_CANCEL);\n                        mFastScrollable.superOnTouchEvent(fakeCancelMotionEvent);\n                        fakeCancelMotionEvent.recycle();\n                        // update ThumbDrawable state and report\n                        // OnFastScrollListener\n                        setPressedThumb(true);\n                        // Call updateThumbRect to report\n                        // OnFastScrollListener.onFastScrolled\n                        updateThumbRect(0, true);\n                        // Do NOT fade Thumb\n                        mView.removeCallbacks(mScrollCache);\n                    }\n                }\n                break;\n            }\n            case MotionEvent.ACTION_MOVE: {\n                if (mIsHanlingTouchEvent) {\n                    final int touchDeltaY = Math.round(y - mDownY);\n                    if (touchDeltaY != 0) {\n                        updateThumbRect(touchDeltaY);\n                        // only touchDeltaY != 0, we save the touchY, to Avoid\n                        // accuracy error\n                        mDownY = y;\n                    }\n                }\n                break;\n            }\n            case MotionEvent.ACTION_UP:\n            case MotionEvent.ACTION_CANCEL: {\n                if (mIsHanlingTouchEvent) {\n                    setPressedThumb(false);\n                    mIsHanlingTouchEvent = false;\n                    awakenScrollBars();\n                }\n                break;\n            }\n            default:\n                break;\n        }// End switch\n        if (mIsHanlingTouchEvent) {\n            mView.invalidate();\n            mView.getParent().requestDisallowInterceptTouchEvent(true);\n            return true;\n        }\n        return false;\n    }\n\n    // ===========================================================\n    // Delegate\n    // ===========================================================\n\n    /**\n     * Call after View.dispatchDraw()\n     **/\n    public void dispatchDrawOver(Canvas canvas) {\n        onDrawScrollBars(canvas);\n    }\n\n    public void onAttachedToWindow() {\n        initialAwakenScrollBars();\n    }\n\n    @SuppressLint(\"MissingSuperCall\")\n    // fuck this lint warning\n    public void onDetachedFromWindow() {\n        if (mIndicatorPopup != null) {\n            mIndicatorPopup.dismiss();\n        }\n    }\n\n    /**\n     * Please check if the delegate is NULL before call this method If your view\n     * has the android:visibility attr in xml, this method in view is called\n     * before your delegate is created\n     */\n    public void onVisibilityChanged(View changedView, int visibility) {\n        if (visibility == View.VISIBLE) {\n            // This compat method is interesting, KK has method\n            // isAttachedToWindow\n            // < KK is view.getWindowToken() != null\n            if (ViewCompat.isAttachedToWindow(mView)) {\n                // Same as mAttachInfo != null\n                initialAwakenScrollBars();\n            }\n\n        }\n    }\n\n    public void onWindowVisibilityChanged(int visibility) {\n        if (visibility == View.VISIBLE) {\n            initialAwakenScrollBars();\n        }\n    }\n\n    // ===========================================================\n    // Internal\n    // ===========================================================\n\n    private void onDrawScrollBars(Canvas canvas) {\n        boolean invalidate = false;\n        if (mIsHanlingTouchEvent) {\n            mThumbDrawable.setAlpha(255);\n        } else {\n            // Copy from View.class\n            final ScrollabilityCache cache = mScrollCache;\n            // cache.scrollBar = mThumbDrawable;\n            final int state = cache.state;\n            if (state == ScrollabilityCache.OFF) {\n                return;\n            }\n            if (state == ScrollabilityCache.FADING) {\n                // We're fading -- get our fade interpolation\n                if (cache.interpolatorValues == null) {\n                    cache.interpolatorValues = new float[1];\n                }\n                float[] values = cache.interpolatorValues;\n                // Stops the animation if we're done\n                if (cache.scrollBarInterpolator.timeToValues(values) == Interpolator.Result.FREEZE_END) {\n                    cache.state = ScrollabilityCache.OFF;\n                } else {\n                    // in View.class is \"cache.scrollBar.mutate()\"\n                    mThumbDrawable.setAlpha(Math.round(values[0]));\n                }\n                invalidate = true;\n            } else {\n                // reset alpha, in View.class is \"cache.scrollBar.mutate()\"\n                mThumbDrawable.setAlpha(255);\n            }\n        }\n\n        // Draw the thumb\n        if (updateThumbRect(0)) {\n            final int scrollY = mView.getScrollY();\n            final int scrollX = mView.getScrollX();\n            mThumbDrawable.setBounds(mThumbRect.left + scrollX, mThumbRect.top + scrollY, mThumbRect.right + scrollX,\n                    mThumbRect.bottom + scrollY);\n            mThumbDrawable.draw(canvas);\n        }\n        if (invalidate) {\n            mView.invalidate();\n        }\n\n    }\n\n    private void setPressedThumb(boolean pressed) {\n        mThumbDrawable.setState(pressed ? DRAWABLE_STATE_PRESSED : DRAWABLE_STATE_DEFAULT);\n        mView.invalidate();\n        if (mIndicatorPopup != null) {\n            if (pressed) {\n                mIndicatorPopup.show();\n            } else {\n                mIndicatorPopup.dismiss();\n            }\n        }\n        if (mFastScrollListener != null) {\n            if (pressed) {\n                mFastScrollListener.onFastScrollStart(mView, this);\n            } else {\n                mFastScrollListener.onFastScrollEnd(mView, this);\n            }\n\n        }\n    }\n\n    private boolean updateThumbRect(int touchDeltaY) {\n        return updateThumbRect(touchDeltaY, false);\n    }\n\n    /**\n     * updateThumbRect\n     *\n     * @param touchDeltaY             ,if touchDeltaY != 0, will report\n     *                                FastScrollListener.onFastScrolled\n     * @param forceReportFastScrolled , if true, will force report FastScrollListener.onFastScrolled\n     * @return false:Thumb return false means no need to draw thumb\n     */\n    private boolean updateThumbRect(int touchDeltaY, boolean forceReportFastScrolled) {\n        final int thumbWidth = mThumbRect.width();\n        mThumbRect.right = mView.getWidth();\n        mThumbRect.left = mThumbRect.right - thumbWidth;\n        final int scrollRange = mFastScrollable.superComputeVerticalScrollRange();// 整体的全部高度\n        if (scrollRange <= 0) {// no content, 仅在有内容的时候绘制thumb\n            return false;\n        }\n        final int scrollOffset = mFastScrollable.superComputeVerticalScrollOffset();// 上方已经滑动出本身范围的高度\n        final int scrollExtent = mFastScrollable.superComputeVerticalScrollExtent();// 当前显示区域的高度\n        final int scrollMaxOffset = scrollRange - scrollExtent;\n        if (scrollMaxOffset <= 0) {// can not scroll, 内容部分不够或刚好充满\n            return false;\n        }\n        final float scrollPercent = scrollOffset * 1f / (scrollMaxOffset);\n        final float visiblePercent = scrollExtent * 1f / scrollRange;\n        // log(\"scrollPercent->\" + scrollPercent + \" visiblePercent->\" +\n        // visiblePercent);\n        final int viewHeight = mView.getHeight();\n        final int thumbHeight = mThumbDynamicHeight ? Math\n                .max(mThumbMinHeight, Math.round(visiblePercent * viewHeight)) : mThumbMinHeight;\n        mThumbRect.bottom = mThumbRect.top + thumbHeight;\n        final int thumbTop = Math.round((viewHeight - thumbHeight) * scrollPercent);\n        mThumbRect.offsetTo(mThumbRect.left, thumbTop);\n\n        if (mIndicatorPopup != null) {\n            mIndicatorPopup.setOffset(mView.getWidth() - mIndicatorPopup.getPopupSize() - mThumbRect.width(),\n                    -viewHeight + mThumbRect.centerY() - mIndicatorPopup.getPopupSize());\n        }\n        if (touchDeltaY != 0) {// compute the ScrollOffset, 按touchDeltaY计算滚动\n            int newThumbTop = thumbTop + touchDeltaY;\n            final int minThumbTop = 0;\n            final int maxThumbTop = viewHeight - thumbHeight;\n            if (newThumbTop > maxThumbTop) {\n                newThumbTop = maxThumbTop;\n            } else if (newThumbTop < minThumbTop) {\n                newThumbTop = minThumbTop;\n            }\n\n            final float newScrollPercent = newThumbTop * 1f / maxThumbTop;// 百分比\n            final int newScrollOffset = Math.round((scrollRange - scrollExtent) * newScrollPercent);\n            final int viewScrollDeltaY = newScrollOffset - scrollOffset;\n            if (mView instanceof AbsListView) {\n                // Call scrollBy to AbsListView , not work correctly\n                ((AbsListView) mView).smoothScrollBy(viewScrollDeltaY, 0);\n            } else {\n                mView.scrollBy(0, viewScrollDeltaY);\n\n            }\n            if (mFastScrollListener != null) {\n                mFastScrollListener.onFastScrolled(mView, this, touchDeltaY, viewScrollDeltaY, newScrollPercent);\n            }\n        } else {\n            if (forceReportFastScrolled) {\n                if (mFastScrollListener != null) {\n                    mFastScrollListener.onFastScrolled(mView, this, 0, 0, scrollPercent);\n                }\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Copy from View.class\n     **/\n    private static class ScrollabilityCache implements Runnable {\n        /*** Scrollbars are not visible */\n        public static final int OFF = 0;\n        /**\n         * Scrollbars are visible\n         */\n        public static final int ON = 1;\n        /**\n         * Scrollbars are fading away\n         */\n        public static final int FADING = 2;\n        public final int scrollBarDefaultDelayBeforeFade;\n        public final int scrollBarFadeDuration;\n\n        // public ScrollBarDrawable scrollBar;\n        // public Drawable scrollBar;\n        public float[] interpolatorValues;\n        public View host;\n\n        public final Interpolator scrollBarInterpolator = new Interpolator(1, 2);\n\n        private static final float[] OPAQUE = {255};\n        private static final float[] TRANSPARENT = {0.0f};\n\n        /**\n         * When fading should start. This time moves into the future every time\n         * a new scroll happens. Measured based on SystemClock.uptimeMillis()\n         */\n        public long fadeStartTime;\n\n        /**\n         * The current state of the scrollbars: ON, OFF, or FADING\n         */\n        public int state = OFF;\n\n        public ScrollabilityCache(ViewConfiguration configuration, View host) {\n            // scrollBarSize = configuration.getScaledScrollBarSize();\n            scrollBarDefaultDelayBeforeFade = ViewConfiguration.getScrollDefaultDelay();\n            scrollBarFadeDuration = ViewConfiguration.getScrollBarFadeDuration();\n            this.host = host;\n        }\n\n        public void run() {\n            long now = AnimationUtils.currentAnimationTimeMillis();\n            if (now >= fadeStartTime) {\n\n                // the animation fades the scrollbars out by changing\n                // the opacity (alpha) from fully opaque to fully\n                // transparent\n                int nextFrame = (int) now;\n                int framesCount = 0;\n\n                Interpolator interpolator = scrollBarInterpolator;\n\n                // Start opaque\n                interpolator.setKeyFrame(framesCount++, nextFrame, OPAQUE);\n\n                // End transparent\n                nextFrame += scrollBarFadeDuration;\n                interpolator.setKeyFrame(framesCount, nextFrame, TRANSPARENT);\n\n                state = FADING;\n\n                // Kick off the fade animation\n                // host.invalidate(true);\n                host.invalidate();\n            }\n        }\n    }\n\n    public View getView() {\n        return mView;\n    }\n\n    private int dp2px(float dp) {\n        return (int) (mDensity * dp + 0.5f);\n    }\n\n    public void setIndicatorText(String indicator) {\n        if (mIndicatorPopup != null) {\n            mIndicatorPopup.setIndicatorText(indicator);\n        }\n    }\n\n    public void initIndicatorPopup(IndicatorPopup indicatorPopup) {\n        mIndicatorPopup = indicatorPopup;\n    }\n\n    // ===========================================================\n    // IndicatorPopup\n    // ===========================================================\n    public static class IndicatorPopup {\n\n        public static class Builder {\n\n            private final float density;\n            private final View anchor;\n            private int indicatorPopupColor = COLOR_THUMB_PRESSED;\n            private int indicatorPopupSize;\n            private int indicatorTextSize;\n            private int indicatorMarginRight;\n            private int indicatorPopupAnimationStyle = FASTSCROLLER_INDICATOR_POPUPANIMATIONSTYLE;\n\n            public Builder(FastScrollDelegate delegate) {\n                this.anchor = delegate.getView();\n                this.density = anchor.getContext().getResources().getDisplayMetrics().density;\n                indicatorPopupSize = dp2px(FASTSCROLLER_INDICATOR_SIZE_DP);\n                indicatorTextSize = dp2px(FASTSCROLLER_INDICATOR_TEXTSIZE_DP);\n                indicatorMarginRight = dp2px(FASTSCROLLER_INDICATOR_MARINRIGHT_DP);\n            }\n\n            public Builder indicatorPopupColor(int popupColor) {\n                indicatorPopupColor = popupColor;\n                return this;\n            }\n\n            public Builder indicatorPopupSize(int popupSizeDp) {\n                indicatorPopupSize = dp2px(popupSizeDp);\n                return this;\n            }\n\n            public Builder indicatorTextSize(int textSizeDp) {\n                indicatorTextSize = dp2px(textSizeDp);\n                return this;\n            }\n\n            public Builder indicatorMarginRight(int marginRightDp) {\n                indicatorMarginRight = dp2px(marginRightDp);\n                return this;\n            }\n\n            public Builder indicatorPopupAnimationStyle(int animationStyle) {\n                indicatorPopupAnimationStyle = animationStyle;\n                return this;\n            }\n\n            private int dp2px(float dp) {\n                return (int) (density * dp + 0.5f);\n            }\n\n            public IndicatorPopup build() {\n                return new IndicatorPopup(anchor, indicatorPopupColor, indicatorPopupSize, indicatorTextSize,\n                        indicatorMarginRight, indicatorPopupAnimationStyle);\n            }\n\n        }\n\n        final View anchor;\n        final int popupSize;\n        final int marginRight;\n        final TextView bubbleView;\n        int xOffset, yOffset;\n        final PopupWindow popupWindow;\n\n        @SuppressWarnings(\"deprecation\")\n        private IndicatorPopup(View anchor, int popupColor, int popupSize, int textSize, int marginRight,\n                               int popupAnimationStyle) {\n            super();\n            this.anchor = anchor;\n            this.popupSize = popupSize;\n            this.marginRight = marginRight;\n            this.bubbleView = new TextView(anchor.getContext());\n            bubbleView.setGravity(Gravity.CENTER);\n            bubbleView.setTextColor(Color.WHITE);\n            bubbleView.setSingleLine();\n            bubbleView.setBackgroundDrawable(new BubbleDrawable(popupColor));\n            bubbleView.setEllipsize(TruncateAt.END);\n            bubbleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);\n            this.popupWindow = new PopupWindow(bubbleView, popupSize, popupSize, false);\n            popupWindow.setAnimationStyle(popupAnimationStyle);\n        }\n\n        public int getPopupSize() {\n            return popupSize;\n        }\n\n        public void setOffset(int xoff, int yoff) {\n            this.xOffset = xoff;\n            this.yOffset = yoff;\n            if (popupWindow != null && popupWindow.isShowing()) {\n                popupWindow.update(anchor, xoff - marginRight, yoff, popupSize, popupSize);\n            }\n\n        }\n\n        public void show() {\n            if (popupWindow != null && !popupWindow.isShowing()) {\n                popupWindow.showAsDropDown(anchor, xOffset - marginRight, yOffset);\n            }\n        }\n\n        public void dismiss() {\n            if (popupWindow != null && popupWindow.isShowing()) {\n                popupWindow.dismiss();\n            }\n        }\n\n        public void setIndicatorText(String indicator) {\n            bubbleView.setText(indicator);\n        }\n    }\n\n    private static class BubbleDrawable extends Drawable {\n\n        private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);\n        private final Path path = new Path();\n        private final RectF rectF = new RectF();\n\n        public BubbleDrawable(int color) {\n            super();\n            paint.setColor(color);\n        }\n\n        @Override\n        public void draw(Canvas canvas) {\n            canvas.drawPath(path, paint);\n        }\n\n        @Override\n        protected void onBoundsChange(Rect bounds) {\n            super.onBoundsChange(bounds);\n            final int w = bounds.width();\n            final int h = bounds.height();\n            path.reset();\n            final float radius = Math.min(w, h) / 2f - 1f;\n            final float cx = w / 2f;\n            final float cy = h / 2f;\n            rectF.set(cx - radius, cy - radius, cx + radius, cy + radius);\n            // path.addArc()//Not work\n            path.arcTo(rectF, 0, -270, true);\n            path.lineTo(cx + radius, cy + radius);\n            path.close();\n        }\n\n        @Override\n        public void setAlpha(int alpha) {\n            paint.setAlpha(alpha);\n        }\n\n        @Override\n        public void setColorFilter(ColorFilter cf) {\n            paint.setColorFilter(cf);\n        }\n\n        @Override\n        public int getOpacity() {\n            return PixelFormat.TRANSLUCENT;\n        }\n\n    }\n\n    // ==================\n    // Builder\n    // ==================\n\n    public static int FASTSCROLLER_WIDTH_DP = 20;// 12;\n    public static int FASTSCROLLER_MIN_HEIGHT_DP = 32;\n    public static int FASTSCROLLER_THUMB_WIDTH = 4;\n    public static int FASTSCROLLER_THUMB_INSET_TOP_BOTTOM_RIGHT = 4;\n\n    public static int COLOR_THUMB_NORMAL = 0x80808080;\n    public static int COLOR_THUMB_PRESSED = 0xff03a9f4;// 0xff45c01a;\n\n    private static int FASTSCROLLER_INDICATOR_SIZE_DP = 72;\n    private static int FASTSCROLLER_INDICATOR_MARINRIGHT_DP = 24;\n    private static int FASTSCROLLER_INDICATOR_TEXTSIZE_DP = 36;\n    private static int FASTSCROLLER_INDICATOR_POPUPANIMATIONSTYLE = android.R.style.Animation_Dialog;\n\n    public static class Builder {\n        private final float density;\n        private final FastScrollable fastScrollable;\n        private int width;\n        private int height;\n        private boolean isDynamicHeight = true;\n        private Drawable thumbDrawable;\n        private int thumbNormalColor = COLOR_THUMB_NORMAL;\n        private int thumbPressedColor = COLOR_THUMB_PRESSED;\n\n        public Builder(FastScrollable fastScrollable) {\n            super();\n            this.fastScrollable = fastScrollable;\n            this.density = fastScrollable.getFastScrollableView().getContext().getResources().getDisplayMetrics().density;\n            width = dp2px(FASTSCROLLER_WIDTH_DP);\n            height = dp2px(FASTSCROLLER_MIN_HEIGHT_DP);\n\n        }\n\n        public Builder width(float widthDp) {\n            width = dp2px(widthDp);\n            return this;\n        }\n\n        public Builder height(float heightDp) {\n            height = dp2px(heightDp);\n            return this;\n        }\n\n        public Builder thumbNormalColor(int normalColor) {\n            thumbNormalColor = normalColor;\n            return this;\n        }\n\n        public Builder thumbPressedColor(int pressedColor) {\n            thumbPressedColor = pressedColor;\n            return this;\n        }\n\n        public Builder thumbDrawable(Drawable thumb) {\n            thumbDrawable = thumb;\n            return this;\n        }\n\n        public Builder dynamicHeight(boolean isDynamic) {\n            isDynamicHeight = isDynamic;\n            return this;\n        }\n\n        public FastScrollDelegate build() {\n            if (this.thumbDrawable == null) {\n                this.thumbDrawable = makeDefaultThumbDrawable();\n            }\n            return new FastScrollDelegate(fastScrollable, width, height, thumbDrawable, isDynamicHeight);\n        }\n\n        private Drawable makeDefaultThumbDrawable() {\n            StateListDrawable stateListDrawable = new StateListDrawable();\n            GradientDrawable pressedDrawable = new GradientDrawable();\n            pressedDrawable.setColor(thumbPressedColor);\n            final float radius = width / 2f;\n            final int inset = dp2px(FASTSCROLLER_THUMB_INSET_TOP_BOTTOM_RIGHT);// inset\n            final int insetLeft = width - inset - dp2px(FASTSCROLLER_THUMB_WIDTH);\n            pressedDrawable.setCornerRadius(radius);\n            stateListDrawable.addState(DRAWABLE_STATE_PRESSED, new InsetDrawable(pressedDrawable, insetLeft, inset,\n                    inset, inset));\n            GradientDrawable normalDrawable = new GradientDrawable();\n            normalDrawable.setColor(thumbNormalColor);\n            normalDrawable.setCornerRadius(radius);\n            stateListDrawable.addState(DRAWABLE_STATE_DEFAULT, new InsetDrawable(normalDrawable, insetLeft, inset,\n                    inset, inset));\n            return stateListDrawable;\n        }\n\n        public int dp2px(float dp) {\n            return (int) (dp * density + 0.5f);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/fastscroll/FastScrollListView.java",
    "content": "package me.wizos.loread.view.fastscroll;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.widget.ListView;\n\n/**\n * https://github.com/Mixiaoxiao/FastScroll-Everywhere FastScrollListView\n *\n * @author Mixiaoxiao 2016-08-31\n */\npublic class FastScrollListView extends ListView implements FastScrollDelegate.FastScrollable {\n\n    private FastScrollDelegate mFastScrollDelegate;\n\n    // ===========================================================\n    // Constructors\n    // ===========================================================\n\n    public FastScrollListView(Context context) {\n        super(context);\n        createFastScrollDelegate(context);\n    }\n\n    public FastScrollListView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        createFastScrollDelegate(context);\n    }\n\n    public FastScrollListView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n        createFastScrollDelegate(context);\n    }\n\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    public FastScrollListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n        super(context, attrs, defStyleAttr, defStyleRes);\n        createFastScrollDelegate(context);\n    }\n\n    // ===========================================================\n    // createFastScrollDelegate\n    // ===========================================================\n\n    private void createFastScrollDelegate(Context context) {\n        mFastScrollDelegate = new FastScrollDelegate.Builder(this).build();\n    }\n\n    // ===========================================================\n    // Delegate\n    // ===========================================================\n\n    @Override\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n        if (mFastScrollDelegate.onInterceptTouchEvent(ev)) {\n            return true;\n        }\n        return super.onInterceptTouchEvent(ev);\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        if (mFastScrollDelegate.onTouchEvent(event)) {\n            return true;\n        }\n        return super.onTouchEvent(event);\n    }\n\n    @Override\n    protected void onAttachedToWindow() {\n        super.onAttachedToWindow();\n        mFastScrollDelegate.onAttachedToWindow();\n    }\n\n    @Override\n    protected void onVisibilityChanged(View changedView, int visibility) {\n        super.onVisibilityChanged(changedView, visibility);\n        if (mFastScrollDelegate != null) {\n            mFastScrollDelegate.onVisibilityChanged(changedView, visibility);\n        }\n    }\n\n    @Override\n    protected void onWindowVisibilityChanged(int visibility) {\n        super.onWindowVisibilityChanged(visibility);\n        mFastScrollDelegate.onWindowVisibilityChanged(visibility);\n    }\n\n    @Override\n    protected boolean awakenScrollBars() {\n        return mFastScrollDelegate.awakenScrollBars();\n    }\n\n    @Override\n    protected void dispatchDraw(Canvas canvas) {\n        super.dispatchDraw(canvas);\n        mFastScrollDelegate.dispatchDrawOver(canvas);\n    }\n\n    // ===========================================================\n    // FastScrollable IMPL, ViewInternalSuperMethods\n    // ===========================================================\n\n    @Override\n    public void superOnTouchEvent(MotionEvent event) {\n        super.onTouchEvent(event);\n    }\n\n    @Override\n    public int superComputeVerticalScrollExtent() {\n        return super.computeVerticalScrollExtent();\n    }\n\n    @Override\n    public int superComputeVerticalScrollOffset() {\n        return super.computeVerticalScrollOffset();\n    }\n\n    @Override\n    public int superComputeVerticalScrollRange() {\n        return super.computeVerticalScrollRange();\n    }\n\n    @Override\n    public View getFastScrollableView() {\n        return this;\n    }\n\n    /**\n     * @deprecated use {@link #getFastScrollDelegate()} instead\n     */\n    public FastScrollDelegate getDelegate() {\n        return getFastScrollDelegate();\n    }\n\n    @Override\n    public FastScrollDelegate getFastScrollDelegate() {\n        return mFastScrollDelegate;\n    }\n\n    @Override\n    public void setNewFastScrollDelegate(FastScrollDelegate newDelegate) {\n        if (newDelegate == null) {\n            throw new IllegalArgumentException(\"setNewFastScrollDelegate must NOT be NULL.\");\n        }\n        mFastScrollDelegate.onDetachedFromWindow();\n        mFastScrollDelegate = newDelegate;\n        newDelegate.onAttachedToWindow();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/fastscroll/FastScrollRecyclerView.java",
    "content": "package me.wizos.loread.view.fastscroll;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.View;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * https://github.com/Mixiaoxiao/FastScroll-Everywhere FastScrollRecyclerView\n *\n * @author Mixiaoxiao 2016-08-31\n */\npublic class FastScrollRecyclerView extends RecyclerView implements FastScrollDelegate.FastScrollable {\n\n    private FastScrollDelegate mFastScrollDelegate;\n\n    // ===========================================================\n    // Constructors\n    // ===========================================================\n\n    public FastScrollRecyclerView(Context context) {\n        super(context);\n        createFastScrollDelegate(context);\n    }\n\n    public FastScrollRecyclerView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        createFastScrollDelegate(context);\n    }\n\n    public FastScrollRecyclerView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n        createFastScrollDelegate(context);\n    }\n\n    // ===========================================================\n    // createFastScrollDelegate\n    // ===========================================================\n\n    private void createFastScrollDelegate(Context context) {\n        mFastScrollDelegate = new FastScrollDelegate.Builder(this).build();\n    }\n\n    // ===========================================================\n    // Delegate\n    // ===========================================================\n\n    @Override\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n        if (mFastScrollDelegate.onInterceptTouchEvent(ev)) {\n            return true;\n        }\n        return super.onInterceptTouchEvent(ev);\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        if (mFastScrollDelegate.onTouchEvent(event)) {\n            return true;\n        }\n        return super.onTouchEvent(event);\n    }\n\n    @Override\n    protected void onAttachedToWindow() {\n        super.onAttachedToWindow();\n        mFastScrollDelegate.onAttachedToWindow();\n    }\n\n    @Override\n    protected void onVisibilityChanged(View changedView, int visibility) {\n        super.onVisibilityChanged(changedView, visibility);\n        if (mFastScrollDelegate != null) {\n            mFastScrollDelegate.onVisibilityChanged(changedView, visibility);\n        }\n    }\n\n    @Override\n    protected void onWindowVisibilityChanged(int visibility) {\n        super.onWindowVisibilityChanged(visibility);\n        mFastScrollDelegate.onWindowVisibilityChanged(visibility);\n    }\n\n    @Override\n    protected boolean awakenScrollBars() {\n        return mFastScrollDelegate.awakenScrollBars();\n    }\n\n    @Override\n    protected void dispatchDraw(Canvas canvas) {\n        super.dispatchDraw(canvas);\n        mFastScrollDelegate.dispatchDrawOver(canvas);\n    }\n\n    // ===========================================================\n    // FastScrollable IMPL, ViewInternalSuperMethods\n    // ===========================================================\n\n    @Override\n    public void superOnTouchEvent(MotionEvent event) {\n        super.onTouchEvent(event);\n    }\n\n    @Override\n    public int superComputeVerticalScrollExtent() {\n        return super.computeVerticalScrollExtent();\n    }\n\n    @Override\n    public int superComputeVerticalScrollOffset() {\n        return super.computeVerticalScrollOffset();\n    }\n\n    @Override\n    public int superComputeVerticalScrollRange() {\n        return super.computeVerticalScrollRange();\n    }\n\n    @Override\n    public View getFastScrollableView() {\n        return this;\n    }\n\n    /**\n     * @deprecated use {@link #getFastScrollDelegate()} instead\n     */\n    public FastScrollDelegate getDelegate() {\n        return getFastScrollDelegate();\n    }\n\n    @Override\n    public FastScrollDelegate getFastScrollDelegate() {\n        return mFastScrollDelegate;\n    }\n\n    @Override\n    public void setNewFastScrollDelegate(FastScrollDelegate newDelegate) {\n        if (newDelegate == null) {\n            throw new IllegalArgumentException(\"setNewFastScrollDelegate must NOT be NULL.\");\n        }\n        mFastScrollDelegate.onDetachedFromWindow();\n        mFastScrollDelegate = newDelegate;\n        newDelegate.onAttachedToWindow();\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/fastscroll/ListViewS.java",
    "content": "//package me.wizos.loreadx.view.listview;\n//\n//import android.content.Context;\n//import android.os.Handler;\n//import android.os.Message;\n//import android.util.AttributeSet;\n//import android.view.MotionEvent;\n//import android.view.View;\n//import android.widget.AdapterView;\n//\n//import com.ditclear.swipelayout.SwipeDragLayout;\n//import com.socks.library.KLog;\n//\n///**\n// * Created by Wizos on 2017/12/24.\n// */\n//\n//public class ListViewS extends FastScrollListView implements Handler.Callback, SwipeDragLayout.SwipeListener {\n//\n//    /* handler */\n//    private Handler mHandler;\n//\n//    public ListViewS(Context context) {\n//        this(context, null);\n//    }\n//\n//    public ListViewS(Context context, AttributeSet attrs) {\n//        this(context, attrs, 0);\n//    }\n//\n//    public ListViewS(Context context, AttributeSet attrs, int defStyleAttr) {\n//        super(context, attrs, defStyleAttr);\n//        mHandler = new Handler(this);\n//    }\n//\n//\n//    /**\n//     * 自己写的长点击事件\n//     */\n//    /* Handler 的 Message 信息 */\n//    private static final int MSG_WHAT_LONG_CLICK = 1;\n//\n//    /* onTouch里面的状态 */\n//    private static final int STATE_NOTHING = -1;//抬起状态\n//    private static final int STATE_DOWN = 0;//按下状态\n//    private static final int STATE_LONG_CLICK = 1;//长点击状态\n//    //    private static final int STATE_SCROLL = 2;//SCROLL状态\n//    private static final int STATE_LONG_CLICK_FINISH = 3;//长点击已经触发完成\n//    //    private static final int STATE_MORE_FINGERS = 4;//多个手指\n//    private int mState = STATE_NOTHING;\n//    private OnListItemLongClickListener mOnListItemLongClickListener;\n//\n//    public void setOnListItemLongClickListener(OnListItemLongClickListener listener) {\n//        mOnListItemLongClickListener = listener;\n//    }\n//\n//    public interface OnListItemLongClickListener {\n//        void onListItemLongClick(View view, int position);\n//    }\n//\n//    @Override\n//    public boolean handleMessage(Message msg) {\n//        switch (msg.what) {\n//            case MSG_WHAT_LONG_CLICK:\n//                //如果得到msg的时候state状态是Long Click的话\n//                if (mState == STATE_DOWN || mState == STATE_LONG_CLICK) {\n//                    //改为long click触发完成\n//                    mState = STATE_LONG_CLICK_FINISH;\n//                    //得到长点击的位置\n//                    int position = msg.arg1;\n//                    //找到那个位置的view\n//                    View view = getChildAt(position - getFirstVisiblePosition());\n//                    //如果设置了监听器的话，就触发\n//                    if (mOnListItemLongClickListener != null && position == pointToPosition(lastX, lastY)) {\n//                        mOnListItemLongClickListener.onListItemLongClick(view, position);\n//                        KLog.e(\"==\" + msg.what);\n////                        mVibrator.vibrate(100); // 触发震动\n//                    }\n//                }\n//                break;\n//            default:\n//                break;\n//        }\n//        return true;\n//    }\n//\n//    /* 手指放下的坐标 */\n//    private int downX;\n//    private int downY;\n//    /* Handler 发送message需要延迟的时间 */\n//    private static final long CLICK_LONG_TRIGGER_TIME = 500;//1s\n//\n//    /**\n//     * remove message\n//     */\n//    private void removeLongClickMessage() {\n//        if (mHandler.hasMessages(MSG_WHAT_LONG_CLICK)) {\n//            mHandler.removeMessages(MSG_WHAT_LONG_CLICK);\n//        }\n//    }\n//\n//    /**\n//     * send message\n//     */\n//    private void sendLongClickMessage(int position) {\n//        if (!mHandler.hasMessages(MSG_WHAT_LONG_CLICK)) {\n//            Message message = new Message();\n//            message.what = MSG_WHAT_LONG_CLICK;\n//            message.arg1 = position;\n//            mHandler.sendMessageDelayed(message, CLICK_LONG_TRIGGER_TIME);\n//        }\n//    }\n//\n////    @Override\n////    public boolean onInterceptTouchEvent(MotionEvent ev) {\n////        switch (ev.getAction()) {\n////            case MotionEvent.ACTION_DOWN:\n////                break;\n////            case MotionEvent.ACTION_MOVE:\n////                // 容差值大概是24，再加上60\n////                if (fingerLeftAndRightMove(ev)) {\n////                    return false;\n////                }\n////                break;\n////        }\n////        return super.onInterceptTouchEvent(ev);\n////    }\n//\n//    private int slideItemPosition = -1;\n//    private int lastX;\n//    private int lastY;\n//\n//    @Override\n//    public boolean dispatchTouchEvent(MotionEvent ev) {\n//        lastX = (int) ev.getX();\n//        lastY = (int) ev.getY();\n//        switch (ev.getAction()) { // & MotionEvent.ACTION_MASK\n//            case MotionEvent.ACTION_DOWN:\n//                //获取出坐标来\n//                downX = (int) ev.getX();\n//                downY = (int) ev.getY();\n//                //当前state状态为按下\n//                mState = STATE_DOWN;\n//                sendLongClickMessage(pointToPosition(downX, downY)); // FIXME: 2016/5/4 【添加】修复 长按 bug\n//                break;\n//            // 这个是实现多点的关键，当屏幕检测到有多个手指同时按下之后，就触发了这个事件\n//            case MotionEvent.ACTION_POINTER_DOWN:\n//                removeLongClickMessage();\n////                mState = STATE_MORE_FINGERS;\n//                //消耗掉，不传递下去了\n//                return true;\n//\n//            case MotionEvent.ACTION_MOVE:\n//                if (fingerNotMove(ev)) {//手指的范围在50以内\n////                   removeLongClickMessage();\n////                   return true;\n//                } else if (fingerLeftAndRightMove(ev)) {\n//                    removeLongClickMessage();\n//                    //将当前想要滑动哪一个传递给wrapperAdapter\n//                    int position = pointToPosition(downX, downY);\n//                    if (position != AdapterView.INVALID_POSITION) {\n//                        slideItemPosition = position;\n//                    }\n//                } else {\n//                    removeLongClickMessage();\n//                }\n//                break;\n//            case MotionEvent.ACTION_UP:\n//            case MotionEvent.ACTION_CANCEL:\n//                removeLongClickMessage();\n//                break;\n//            default:\n//                break;\n//        }\n//        return super.dispatchTouchEvent(ev);\n//    }\n//\n//    /* 手指滑动的最短距离 */\n//    private int mShortestDistance = 25;\n//\n//    /**\n//     * 上下左右不能超出50\n//     *\n//     * @param ev\n//     * @return\n//     */\n//    private boolean fingerNotMove(MotionEvent ev) {\n//        return (downX - ev.getX() < mShortestDistance && downX - ev.getX() > -mShortestDistance &&\n//                downY - ev.getY() < mShortestDistance && downY - ev.getY() > -mShortestDistance);\n//    }\n//\n//    /**\n//     * 左右得超出50，上下不能超出50\n//     *\n//     * @param ev\n//     * @return\n//     */\n//    private boolean fingerLeftAndRightMove(MotionEvent ev) {\n//        return ((ev.getX() - downX > mShortestDistance || ev.getX() - downX < -mShortestDistance) &&\n//                ev.getY() - downY < mShortestDistance && ev.getY() - downY > -mShortestDistance);\n//    }\n//\n//    /**\n//     * 设置列表项左右滑动时的监听器\n//     *\n//     * @param onItemSlideListener\n//     */\n//    public void setItemSlideListener(OnItemSlideListener onItemSlideListener) {\n//        mOnItemSlideListener = onItemSlideListener;\n//    }\n//\n//    private OnItemSlideListener mOnItemSlideListener;\n//\n//    public interface OnItemSlideListener {\n//        //        int onSlideOpen(View view, int position, int direction);\n//        // FIXME: 2016/5/4 【实现划开自动返回】把返回类型由 void 改为 int\n//        void onUpdate(View view, int position, float offset);\n//\n//        void onCloseLeft(View view, int position, int direction);\n//\n//        void onCloseRight(View view, int position, int direction);\n//\n//        void onClick(View view, int position);\n//\n//        void log(String layout);\n//    }\n//\n//\n//    @Override\n//    public void onUpdate(View view, float offsetRatio, int offset) {\n//        if (mOnItemSlideListener != null) {\n//            mOnItemSlideListener.onUpdate(view, slideItemPosition, offsetRatio);\n//        }\n//    }\n//\n//    @Override\n//    public void onOpened(View view) {\n//\n//    }\n//\n//    @Override\n//    public void onClosed(View view) {\n//\n//    }\n//\n//    @Override\n//    public void onCloseLeft(View view) {\n//        if (mOnItemSlideListener != null) {\n//            mOnItemSlideListener.onCloseLeft(view, slideItemPosition, SwipeDragLayout.DIRECTION_LEFT);\n//        }\n//    }\n//\n//    @Override\n//    public void onCloseRight(View view) {\n//        if (mOnItemSlideListener != null) {\n////            KLog.e(\"关闭右侧\" + (lastX - downX) + \"==\" + (lastY - downY));\n//            mOnItemSlideListener.onCloseRight(view, slideItemPosition, SwipeDragLayout.DIRECTION_RIGHT);\n//        }\n//    }\n//\n//    @Override\n//    public void onClick(View view) {\n//        if (mOnItemSlideListener != null) {\n//            int position = pointToPosition(downX, downY);\n//            mOnItemSlideListener.onClick(view, position);\n//        }\n//    }\n//\n//    @Override\n//    public void log(String layout) {\n//        mOnItemSlideListener.log(layout);\n//    }\n//}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/slideback/SlideBack.java",
    "content": "package me.wizos.loread.view.slideback;\n\nimport android.app.Activity;\n\nimport androidx.annotation.IntDef;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.WeakHashMap;\n\n/**\n * author : ParfoisMeng\n * time   : 2018/12/19\n * desc   : SlideBack使用类\n */\npublic class SlideBack {\n    // 使用WeakHashMap防止内存泄漏\n    private static WeakHashMap<Activity, SlideBackManager> map = new WeakHashMap<>();\n\n//    /**\n//     * 注册\n//     *\n//     * @param activity 目标Act\n//     * @param callBack 回调\n//     */\n//    public static void register(Activity activity, SlideBackCallBack callBack) {\n//        register(activity, false, callBack);\n//    }\n//\n//    /**\n//     * 注册\n//     *\n//     * @param activity   目标Act\n//     * @param haveScroll 页面是否有滑动\n//     * @param callBack   回调\n//     */\n//    public static void register(Activity activity, boolean haveScroll, SlideBackCallBack callBack) {\n//        with(activity).haveScroll(haveScroll).callBack(callBack).register();\n//    }\n\n    /**\n     * 注销\n     *\n     * @param activity 目标Act\n     */\n    public static void unregister(Activity activity) {\n        SlideBackManager slideBack = map.get(activity);\n        if (null != slideBack) {\n            slideBack.unregister();\n        }\n        map.remove(activity);\n    }\n\n    /**\n     * 构建侧滑管理器 - 用于更丰富的自定义配置\n     *\n     * @param activity 目标Act\n     * @return 构建管理器\n     */\n    public static SlideBackManager with(Activity activity) {\n        SlideBackManager manager = new SlideBackManager(activity);\n        map.put(activity, manager);\n        return manager;\n    }\n\n    /**\n     * 侧滑返回模式 左\n     */\n    public static final int EDGE_LEFT = 0x0000;\n\n    /**\n     * 侧滑返回模式 右\n     */\n    public static final int EDGE_RIGHT = 0x0001;\n\n    /**\n     * 侧滑返回模式 左右皆可\n     */\n    public static final int EDGE_BOTH = 0x0002;\n\n    @IntDef({EDGE_LEFT, EDGE_RIGHT, EDGE_BOTH})\n    @Retention(RetentionPolicy.SOURCE)\n    @interface EdgeMode {\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/slideback/SlideBackManager.java",
    "content": "package me.wizos.loread.view.slideback;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.util.DisplayMetrics;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\n\nimport com.socks.library.KLog;\n\nimport me.wizos.loread.R;\nimport me.wizos.loread.view.slideback.callback.SlideCallBack;\nimport me.wizos.loread.view.slideback.widget.SlideBackIconView;\nimport me.wizos.loread.view.slideback.widget.SlideBackInterceptLayout;\n\nimport static me.wizos.loread.view.slideback.SlideBack.EDGE_BOTH;\nimport static me.wizos.loread.view.slideback.SlideBack.EDGE_LEFT;\nimport static me.wizos.loread.view.slideback.SlideBack.EDGE_RIGHT;\n\n/**\n * author : ParfoisMeng\n * time   : 2018/12/19\n * desc   : SlideBack管理器\n */\npublic class SlideBackManager {\n    private SlideBackIconView slideBackIconViewLeft;\n    private SlideBackIconView slideBackIconViewRight;\n\n    private Activity activity;\n    private boolean haveScroll;\n\n    private SlideCallBack callBack;\n\n    private float backViewHeight; // 控件高度\n    private float arrowSize; // 箭头图标大小\n    private float maxSlideLength; // 最大拉动距离\n\n    // FIXME: 2019/5/1\n    private float sideSlideStartLength; // 侧滑时开始响应的距离\n\n    private float sideSlideLength; // 侧滑响应距离\n    private float dragRate; // 阻尼系数\n\n    private boolean isAllowEdgeLeft; // 使用左侧侧滑\n    private boolean isAllowEdgeRight; // 使用右侧侧滑\n\n    private float screenWidth; // 屏幕宽\n\n    SlideBackManager(Activity activity) {\n        this.activity = activity;\n        haveScroll = false;\n\n        // 获取屏幕信息，初始化控件设置\n        DisplayMetrics dm = activity.getResources().getDisplayMetrics();\n        screenWidth = dm.widthPixels;\n\n        backViewHeight = dm.heightPixels / 4f; // 高度默认 屏高/4\n        arrowSize = dp2px(5); // 箭头大小默认 5dp\n        maxSlideLength = screenWidth / 12; // 最大宽度默认 屏宽/12\n\n        sideSlideLength = maxSlideLength / 2; // 侧滑响应距离默认 控件最大宽度/2\n        sideSlideStartLength = sideSlideLength / 2;\n        dragRate = 3; // 阻尼系数默认 3\n\n        // 侧滑返回模式 默认:左\n        isAllowEdgeLeft = true;\n        isAllowEdgeRight = false;\n    }\n\n    /**\n     * 是否包含滑动控件 默认false\n     */\n    public SlideBackManager haveScroll(boolean haveScroll) {\n        this.haveScroll = haveScroll;\n        return this;\n    }\n\n//    /**\n//     * 回调\n//     */\n//    public SlideBackManager callBack(SlideBackCallBack callBack) {\n//        this.callBack = new SlideCallBack(callBack) {\n//            @Override\n//            public void onSlide(int edgeFrom) {\n//                onSlideBack();\n//            }\n//            @Override\n//            public void onViewSlide(int edgeFrom,int xposition) {\n//            }\n//        };\n//        return this;\n//    }\n\n    /**\n     * 回调 适用于新的左右模式\n     */\n    public SlideBackManager callBack(SlideCallBack callBack) {\n        this.callBack = callBack;\n        return this;\n    }\n\n    /**\n     * 控件高度 默认屏高/4\n     */\n    public SlideBackManager viewHeight(float backViewHeightDP) {\n        this.backViewHeight = dp2px(backViewHeightDP);\n        return this;\n    }\n\n    /**\n     * 箭头大小 默认5dp\n     */\n    public SlideBackManager arrowSize(float arrowSizeDP) {\n        this.arrowSize = dp2px(arrowSizeDP);\n        return this;\n    }\n\n    /**\n     * 最大拉动距离（控件最大宽度） 默认屏宽/12\n     */\n    public SlideBackManager maxSlideLength(float maxSlideLengthDP) {\n        this.maxSlideLength = dp2px(maxSlideLengthDP);\n        return this;\n    }\n\n    /**\n     * 侧滑响应距离 默认控件最大宽度/2\n     */\n    public SlideBackManager sideSlideLength(float sideSlideLengthDP) {\n        this.sideSlideLength = dp2px(sideSlideLengthDP);\n        return this;\n    }\n\n    /**\n     * 阻尼系数 默认3（越小越灵敏）\n     */\n    public SlideBackManager dragRate(float dragRate) {\n        this.dragRate = dragRate;\n        return this;\n    }\n\n    /**\n     * 边缘侧滑模式 默认左\n     */\n    public SlideBackManager edgeMode(@SlideBack.EdgeMode int edgeMode) {\n        switch (edgeMode) {\n            case EDGE_LEFT:\n                isAllowEdgeLeft = true;\n                isAllowEdgeRight = false;\n                break;\n            case EDGE_RIGHT:\n                isAllowEdgeLeft = false;\n                isAllowEdgeRight = true;\n                break;\n            case EDGE_BOTH:\n                isAllowEdgeLeft = true;\n                isAllowEdgeRight = true;\n                break;\n            default:\n                throw new RuntimeException(\"未定义的边缘侧滑模式值：EdgeMode = \" + edgeMode);\n        }\n        return this;\n    }\n\n\n    /**\n     * 需要使用滑动的页面注册\n     */\n    @SuppressLint(\"ClickableViewAccessibility\")\n    public void register() {\n        if (isAllowEdgeLeft) {\n            // 初始化SlideBackIconView 左侧\n            slideBackIconViewLeft = new SlideBackIconView(activity);\n            slideBackIconViewLeft.setBackViewHeight(backViewHeight);\n            slideBackIconViewLeft.setArrowSize(arrowSize);\n            slideBackIconViewLeft.setMaxSlideLength(maxSlideLength);\n        }\n        if (isAllowEdgeRight) {\n            // 初始化SlideBackIconView - Right\n            slideBackIconViewRight = new SlideBackIconView(activity);\n            slideBackIconViewRight.setBackViewHeight(backViewHeight);\n            slideBackIconViewRight.setArrowSize(arrowSize);\n            slideBackIconViewRight.setMaxSlideLength(maxSlideLength);\n            // 右侧侧滑 需要旋转180°\n            slideBackIconViewRight.setRotationY(180);\n        }\n\n\n        // 获取decorView并设置OnTouchListener监听\n        FrameLayout container = (FrameLayout) activity.getWindow().getDecorView().findViewById(R.id.art_slide_layout);\n        if (haveScroll) {\n            SlideBackInterceptLayout interceptLayout = new SlideBackInterceptLayout(activity);\n            //interceptLayout.setSideSlideLength(screenWidth, sideSlideLength);\n\n            float[] triggerZone1 = new float[2];\n            triggerZone1[0] = sideSlideLength / 2;\n            triggerZone1[1] = maxSlideLength;\n            float[] triggerZone2 = new float[2];\n            triggerZone2[0] = screenWidth - sideSlideLength / 2;\n            triggerZone2[1] = screenWidth - maxSlideLength;\n            interceptLayout.addXTriggerZone(triggerZone1);\n            interceptLayout.addXTriggerZone(triggerZone2);\n\n            addInterceptLayout(container, interceptLayout);\n        }\n\n        if (isAllowEdgeLeft) {\n            container.addView(slideBackIconViewLeft);\n        }\n        if (isAllowEdgeRight) {\n            container.addView(slideBackIconViewRight);\n        }\n        for (int i = 0, x = container.getChildCount(); i < x; i++) {\n            KLog.e(\" 子视图：\" + container.getChildAt(i));\n        }\n//        KLog.e(\" 添加箭头：\" + slideBackIconViewLeft.getRight() + \" , \" + slideBackIconViewRight.getRight());\n        KLog.e(\" 是否要添加箭头：\" + isAllowEdgeLeft + \" , \" + isAllowEdgeRight);\n\n        container.setOnTouchListener(new View.OnTouchListener() {\n            private boolean isSideSlideLeft = false;  // 是否从左边边缘开始滑动\n            private boolean isSideSlideRight = false;  // 是否从右边边缘开始滑动\n            private float downX = 0; // 按下的X轴坐标\n            private float moveXLength = 0; // 位移的X轴距离\n\n            @Override\n            public boolean onTouch(View v, MotionEvent event) {\n                switch (event.getAction()) {\n                    case MotionEvent.ACTION_DOWN: // 按下\n                        // 更新按下点的X轴坐标\n                        downX = event.getRawX();\n\n//                        // 检验是否从边缘开始滑动，区分左右\n//                        if (isAllowEdgeLeft && downX <= sideSlideLength) {\n//                            isSideSlideLeft = true;\n//                        } else if (isAllowEdgeRight && downX >= screenWidth - sideSlideLength) {\n//                            isSideSlideRight = true;\n//                        }\n                        // 检验是否从边缘开始滑动，区分左右\n                        if (isAllowEdgeLeft && downX >= sideSlideLength / 2 && downX <= maxSlideLength) {\n                            isSideSlideLeft = true;\n                        } else if (isAllowEdgeRight && downX >= (screenWidth - maxSlideLength) && downX <= (screenWidth - sideSlideLength / 2)) {\n                            isSideSlideRight = true;\n                        }\n//                        KLog.e(\" 是否要添加箭头A：\" + isAllowEdgeLeft + \" , \" + isAllowEdgeRight);\n//                        KLog.e(\" 是否要添加箭头B：\" + (downX <= sideSlideLength) + \" , \" + (downX >= screenWidth - sideSlideLength ));\n//                        KLog.e(\" 是否要添加箭头C：\" + isSideSlideLeft + \" , \" + isSideSlideRight);\n                        break;\n                    case MotionEvent.ACTION_MOVE: // 移动\n                        if (isSideSlideLeft || isSideSlideRight) {\n                            // 从边缘开始滑动\n                            // 获取X轴位移距离\n                            moveXLength = Math.abs(event.getRawX() - downX);\n                            if (moveXLength / dragRate <= maxSlideLength) {\n                                // 如果位移距离在可拉动距离内，更新SlideBackIconView的当前拉动距离并重绘，区分左右\n                                if (isAllowEdgeLeft && isSideSlideLeft) {\n                                    slideBackIconViewLeft.updateSlideLength(moveXLength / dragRate);\n                                    callBack.onViewSlide(EDGE_LEFT, (int) (moveXLength / dragRate));\n                                } else if (isAllowEdgeRight && isSideSlideRight) {\n                                    slideBackIconViewRight.updateSlideLength(moveXLength / dragRate);\n                                    callBack.onViewSlide(EDGE_RIGHT, (int) (moveXLength / dragRate));\n                                }\n                            }\n\n                            // 根据Y轴位置给SlideBackIconView定位\n                            if (isAllowEdgeLeft && isSideSlideLeft) {\n                                setSlideBackPosition(slideBackIconViewLeft, (int) (event.getRawY()));\n                            } else if (isAllowEdgeRight && isSideSlideRight) {\n                                setSlideBackPosition(slideBackIconViewRight, (int) (event.getRawY()));\n                            }\n                        }\n                        break;\n                    case MotionEvent.ACTION_UP: // 抬起\n                        // 是从边缘开始滑动 且 抬起点的X轴坐标大于某值(默认3倍最大滑动长度) 且 回调不为空\n                        if ((isSideSlideLeft || isSideSlideRight) && moveXLength / dragRate >= maxSlideLength && null != callBack) {\n                            // 区分左右\n                            callBack.onSlide(isSideSlideLeft ? EDGE_LEFT : EDGE_RIGHT);\n                        }\n\n                        // 恢复SlideBackIconView的状态\n                        if (isAllowEdgeLeft && isSideSlideLeft) {\n                            slideBackIconViewLeft.updateSlideLength(0);\n                            callBack.onViewSlide(EDGE_LEFT, 0);\n                        } else if (isAllowEdgeRight && isSideSlideRight) {\n                            slideBackIconViewRight.updateSlideLength(0);\n                            callBack.onViewSlide(EDGE_RIGHT, 0);\n                        }\n\n                        // 从边缘开始滑动结束\n                        isSideSlideLeft = false;\n                        isSideSlideRight = false;\n                        break;\n                    default:\n                        break;\n                }\n                return isSideSlideLeft || isSideSlideRight;\n            }\n        });\n    }\n\n    /**\n     * 页面销毁时记得解绑\n     * 其实就是置空防止内存泄漏\n     */\n    @SuppressLint(\"ClickableViewAccessibility\")\n    void unregister() {\n//        FrameLayout container = (FrameLayout) activity.getWindow().getDecorView();\n//        if (haveScroll) removeInterceptLayout(container);\n//        container.removeView(slideBackIconViewLeft);\n//        container.setOnTouchListener(null);\n\n        activity = null;\n        callBack = null;\n        slideBackIconViewLeft = null;\n        slideBackIconViewRight = null;\n    }\n\n    /**\n     * 给根布局包上一层事件拦截处理Layout\n     */\n    private void addInterceptLayout(ViewGroup decorView, SlideBackInterceptLayout interceptLayout) {\n        View rootLayout = decorView.getChildAt(0); // 取出根布局\n        decorView.removeView(rootLayout); // 先移除根布局\n        // 用事件拦截处理Layout将原根布局包起来，再添加回去\n        interceptLayout.addView(rootLayout, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);\n        decorView.addView(interceptLayout);\n    }\n\n    /**\n     * 将根布局还原，移除SlideBackInterceptLayout\n     */\n    private void removeInterceptLayout(ViewGroup decorView) {\n        FrameLayout rootLayout = (FrameLayout) decorView.getChildAt(0); // 取出根布局\n        decorView.removeView(rootLayout); // 先移除根布局\n        // 将根布局的第一个布局(原根布局)取出放回decorView\n        View oriLayout = rootLayout.getChildAt(0);\n        rootLayout.removeView(oriLayout);\n        decorView.addView(oriLayout);\n    }\n\n    /**\n     * 给SlideBackIconView设置topMargin，起到定位效果\n     *\n     * @param view     SlideBackIconView\n     * @param position 触点位置\n     */\n    private void setSlideBackPosition(SlideBackIconView view, int position) {\n        // 触点位置减去SlideBackIconView一半高度即为topMargin\n        int topMargin = (int) (position - (view.getBackViewHeight() / 2));\n        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(view.getLayoutParams());\n        layoutParams.topMargin = topMargin;\n        view.setLayoutParams(layoutParams);\n    }\n\n    private float dp2px(float dpValue) {\n        return dpValue * activity.getResources().getDisplayMetrics().density + 0.5f;\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/slideback/SlideLayout.java",
    "content": "package me.wizos.loread.view.slideback;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.ValueAnimator;\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.util.DisplayMetrics;\nimport android.view.MotionEvent;\nimport android.view.ViewConfiguration;\nimport android.view.animation.DecelerateInterpolator;\nimport android.widget.FrameLayout;\n\nimport androidx.annotation.ColorInt;\n\nimport java.lang.ref.WeakReference;\n\nimport me.wizos.loread.view.slideback.callback.SlideCallBack;\nimport me.wizos.loread.view.slideback.widget.SlideBackIconView;\n\nimport static me.wizos.loread.utils.ScreenUtil.dp2px;\nimport static me.wizos.loread.view.slideback.SlideBack.EDGE_BOTH;\nimport static me.wizos.loread.view.slideback.SlideBack.EDGE_LEFT;\nimport static me.wizos.loread.view.slideback.SlideBack.EDGE_RIGHT;\n\n\n/**\n * @author ditclear on 16/7/12. 可滑动的layout extends FrameLayout\n * https://github.com/ditclear/TimeLine/blob/master/swipelayout/src/main/java/com/ditclear/swipelayout/SwipeDragLayout.java\n * 实现主页的左右滑动已读未读\n */\npublic class SlideLayout extends FrameLayout {\n    private Context context;\n    private int mScaledTouchSlop;\n    private SlideBackIconView slideBackIconViewLeft;\n    private SlideBackIconView slideBackIconViewRight;\n\n    private SlideCallBack callBack;\n\n    private float backViewHeight; // 控件高度\n    private float arrowSize; // 箭头图标大小\n    private int arrowColor; // 箭头图标大小\n    private float maxSlideLength; // 最大拉动距离\n\n    // FIXME: 2019/5/1\n    private float leftViewTriggerStart; // 侧滑时开始响应的距离\n    private float leftViewTriggerEnd;\n    private float rightViewTriggerStart; // 侧滑时开始响应的距离\n    private float rightViewTriggerEnd;\n\n    private float sideSlideLength; // 侧滑响应距离\n    private float dragRate; // 阻尼系数\n\n    private boolean isAllowEdgeLeft; // 使用左侧侧滑\n    private boolean isAllowEdgeRight; // 使用右侧侧滑\n\n\n    public SlideLayout(Context context) {\n        this(context, null);\n    }\n\n    public SlideLayout(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        this.context = context;\n        this.mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();\n\n        // 获取屏幕信息，初始化控件设置\n        DisplayMetrics dm = context.getResources().getDisplayMetrics();\n        float screenWidth = dm.widthPixels;\n\n        backViewHeight = dm.heightPixels / 4f; // 高度默认 屏高/4\n        arrowSize = dp2px(5); // 箭头大小默认 5dp\n        maxSlideLength = screenWidth / 12; // 最大宽度默认 屏宽/12\n\n        sideSlideLength = maxSlideLength / 2; // 侧滑响应距离默认 控件最大宽度/2\n\n        dragRate = 3; // 阻尼系数默认 3\n\n        // 侧滑返回模式 默认:左\n        isAllowEdgeLeft = true;\n        isAllowEdgeRight = true;\n\n        leftViewTriggerStart = sideSlideLength / 2;\n        leftViewTriggerEnd = maxSlideLength * 2;\n        rightViewTriggerStart = screenWidth - maxSlideLength * 2;\n        rightViewTriggerEnd = screenWidth - sideSlideLength / 2;\n\n        mAnimation = ValueAnimator.ofFloat(0f, 1f);\n        mAnimation.setDuration(mDuration);\n        mAnimation.setInterpolator(new DecelerateInterpolator());\n        mAnimUpdateListener = new AnimUpdateListener(this);\n        mAnimListenerAdapter = new AnimListenerAdapter(this);\n    }\n\n\n//    public SlideLayout setAllowEdgeLeft(boolean allowEdgeLeft) {\n//        isAllowEdgeLeft = allowEdgeLeft;\n//        slideBackIconViewLeft = new SlideBackIconView(context);\n//        slideBackIconViewLeft.setBackViewHeight(backViewHeight);\n//        slideBackIconViewLeft.setArrowSize(arrowSize);\n//        slideBackIconViewLeft.setArrowColor(arrowColor);\n//        slideBackIconViewLeft.setMaxSlideLength(maxSlideLength);\n//        addView(slideBackIconViewLeft);\n//        return this;\n//    }\n//\n//    public SlideLayout setAllowEdgeRight(boolean allowEdgeRight) {\n//        isAllowEdgeRight = allowEdgeRight;\n//        slideBackIconViewRight = new SlideBackIconView(context);\n//        slideBackIconViewRight.setBackViewHeight(backViewHeight);\n//        slideBackIconViewRight.setArrowSize(arrowSize);\n//        slideBackIconViewRight.setArrowColor(arrowColor);\n//        slideBackIconViewRight.setMaxSlideLength(maxSlideLength);\n//        // 右侧侧滑 需要旋转180°\n//        slideBackIconViewRight.setRotationY(180);\n//        addView(slideBackIconViewRight);\n//        return this;\n//    }\n\n    /**\n     * 回调 适用于新的左右模式\n     */\n    public SlideLayout callBack(SlideCallBack callBack) {\n        this.callBack = callBack;\n        return this;\n    }\n\n\n    /**\n     * 控件高度 默认屏高/4\n     */\n    public SlideLayout viewHeight(float backViewHeightDP) {\n        this.backViewHeight = dp2px(backViewHeightDP);\n        return this;\n    }\n\n    /**\n     * 箭头大小 默认5dp\n     */\n    public SlideLayout arrowSize(float arrowSizeDP) {\n        this.arrowSize = dp2px(arrowSizeDP);\n        return this;\n    }\n\n    /**\n     * 箭头颜色\n     */\n    public SlideLayout arrowColor(@ColorInt int arrowColor) {\n        this.arrowColor = arrowColor;\n        return this;\n    }\n\n\n    /**\n     * 最大拉动距离（控件最大宽度） 默认屏宽/12\n     */\n    public SlideLayout maxSlideLength(float maxSlideLengthDP) {\n        this.maxSlideLength = dp2px(maxSlideLengthDP);\n        return this;\n    }\n\n    /**\n     * 侧滑响应距离 默认控件最大宽度/2\n     */\n    public SlideLayout sideSlideLength(float sideSlideLengthDP) {\n        this.sideSlideLength = dp2px(sideSlideLengthDP);\n        return this;\n    }\n\n    /**\n     * 阻尼系数 默认3（越小越灵敏）\n     */\n    public SlideLayout dragRate(float dragRate) {\n        this.dragRate = dragRate;\n        return this;\n    }\n\n    /**\n     * 边缘侧滑模式 默认左\n     */\n    public SlideLayout edgeMode(@SlideBack.EdgeMode int edgeMode) {\n        switch (edgeMode) {\n            case EDGE_LEFT:\n                isAllowEdgeLeft = true;\n                isAllowEdgeRight = false;\n                break;\n            case EDGE_RIGHT:\n                isAllowEdgeLeft = false;\n                isAllowEdgeRight = true;\n                break;\n            case EDGE_BOTH:\n                isAllowEdgeLeft = true;\n                isAllowEdgeRight = true;\n                break;\n            default:\n                throw new RuntimeException(\"未定义的边缘侧滑模式值：EdgeMode = \" + edgeMode);\n        }\n        return this;\n    }\n\n\n    /**\n     * 需要使用滑动的页面注册\n     */\n    @SuppressLint(\"ClickableViewAccessibility\")\n    public void register() {\n        if (isAllowEdgeLeft) {\n            // 初始化SlideBackIconView 左侧\n            slideBackIconViewLeft = new SlideBackIconView(context);\n            slideBackIconViewLeft.setBackViewHeight(backViewHeight);\n            slideBackIconViewLeft.setArrowSize(arrowSize);\n            slideBackIconViewLeft.setArrowColor(arrowColor);\n            slideBackIconViewLeft.setMaxSlideLength(maxSlideLength);\n            addView(slideBackIconViewLeft);\n        }\n        if (isAllowEdgeRight) {\n            // 初始化SlideBackIconView - Right\n            slideBackIconViewRight = new SlideBackIconView(context);\n            slideBackIconViewRight.setBackViewHeight(backViewHeight);\n            slideBackIconViewRight.setArrowSize(arrowSize);\n            slideBackIconViewRight.setArrowColor(arrowColor);\n            slideBackIconViewRight.setMaxSlideLength(maxSlideLength);\n            // 右侧侧滑 需要旋转180°\n            slideBackIconViewRight.setRotationY(180);\n            addView(slideBackIconViewRight);\n        }\n        //KLog.e(\" 是否要添加箭头：\" + isAllowEdgeLeft + \" , \" + isAllowEdgeRight);\n    }\n\n\n    private boolean isSideSlideLeft = false;  // 是否从左边边缘开始滑动\n    private boolean isSideSlideRight = false;  // 是否从右边边缘开始滑动\n    private float moveXLength = 0; // 位移的X轴距离\n\n    @Override\n    public boolean onTouchEvent(MotionEvent ev) {\n        switch (ev.getAction()) {\n            case MotionEvent.ACTION_DOWN: // 按下\n                break;\n            case MotionEvent.ACTION_MOVE: // 移动\n                //KLog.e(\"响应手势，移动：\" + isSideSlideLeft + \" , \"  +  isSideSlideRight  + \" , \" + isAllowEdgeLeft + \" , \" + isAllowEdgeRight);\n                if (isSideSlideLeft || isSideSlideRight) {\n                    // 从边缘开始滑动\n                    // 获取X轴位移距离\n                    moveXLength = Math.abs(ev.getRawX() - mDownX);\n                    //KLog.e(\"响应手势，移动B：\" + moveXLength+ \" ,  \" +  mDownX );\n                    if (moveXLength / dragRate <= maxSlideLength) {\n                        // 如果位移距离在可拉动距离内，更新SlideBackIconView的当前拉动距离并重绘，区分左右\n                        if (isAllowEdgeLeft && isSideSlideLeft) {\n                            slideBackIconViewLeft.updateSlideLength(moveXLength / dragRate);\n                            //KLog.e(\"响应手势，移动B：\" + slideBackIconViewLeft.getHeight()+ \" ,  \" +  slideBackIconViewLeft.getVisibility() );\n                            callBack.onViewSlide(EDGE_LEFT, (int) (moveXLength / dragRate));\n                        } else if (isAllowEdgeRight && isSideSlideRight) {\n                            slideBackIconViewRight.updateSlideLength(moveXLength / dragRate);\n                            callBack.onViewSlide(EDGE_RIGHT, (int) (moveXLength / dragRate));\n                        }\n                    }\n\n                    // 根据Y轴位置给SlideBackIconView定位\n                    if (isAllowEdgeLeft && isSideSlideLeft) {\n                        setSlideBackPosition(slideBackIconViewLeft, (int) (ev.getY()));\n                    } else if (isAllowEdgeRight && isSideSlideRight) {\n                        setSlideBackPosition(slideBackIconViewRight, (int) (ev.getY()));\n                    }\n                }\n                break;\n            case MotionEvent.ACTION_UP: // 抬起\n            case MotionEvent.ACTION_CANCEL:\n                // 是从边缘开始滑动 且 抬起点的X轴坐标大于某值(默认3倍最大滑动长度) 且 回调不为空\n                if ((isSideSlideLeft || isSideSlideRight) && moveXLength / dragRate >= maxSlideLength && null != callBack) {\n                    // 区分左右\n                    callBack.onSlide(isSideSlideLeft ? EDGE_LEFT : EDGE_RIGHT);\n                }\n\n                // 恢复SlideBackIconView的状态\n                if (isAllowEdgeLeft && isSideSlideLeft) {\n                    slideBackIconViewLeft.updateSlideLength(0);\n                    callBack.onViewSlide(EDGE_LEFT, 0);\n                } else if (isAllowEdgeRight && isSideSlideRight) {\n                    slideBackIconViewRight.updateSlideLength(0);\n                    callBack.onViewSlide(EDGE_RIGHT, 0);\n                }\n\n                // 从边缘开始滑动结束\n                isSideSlideLeft = false;\n                isSideSlideRight = false;\n                break;\n            default:\n                break;\n        }\n        return isSideSlideLeft || isSideSlideRight;\n    }\n\n    private float mDownX = 0; // 按下的X轴坐标\n    private float mDownY = 0; // 按下的X轴坐标\n\n    @Override\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n        switch (ev.getAction()) {\n            case MotionEvent.ACTION_DOWN: // 按下\n                // 更新按下点的X轴坐标\n                mDownX = ev.getRawX();\n                mDownY = ev.getRawY();\n                break;\n            case MotionEvent.ACTION_MOVE:\n                float offsetX = Math.abs(ev.getRawX() - mDownX);\n                float offsetY = Math.abs(ev.getRawY() - mDownY);\n                if (!(offsetX > mScaledTouchSlop * 2 && offsetY < mScaledTouchSlop)) {\n                    break;\n                }\n                if (isAllowEdgeLeft && (mDownX >= leftViewTriggerStart && mDownX <= leftViewTriggerEnd)) {\n                    isSideSlideLeft = true;\n                    return true;\n                } else if (isAllowEdgeRight && (mDownX >= rightViewTriggerStart && mDownX <= rightViewTriggerEnd)) {\n                    isSideSlideRight = true;\n                    return true;\n                }\n                break;\n        }\n        return super.onInterceptTouchEvent(ev);\n    }\n\n    /**\n     * 给SlideBackIconView设置topMargin，起到定位效果\n     *\n     * @param view     SlideBackIconView\n     * @param position 触点位置\n     */\n    private void setSlideBackPosition(SlideBackIconView view, int position) {\n        // 触点位置减去SlideBackIconView一半高度即为topMargin\n        int topMargin = (int) (position - (view.getBackViewHeight() / 2));\n        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(view.getLayoutParams());\n        layoutParams.topMargin = topMargin;\n        view.setLayoutParams(layoutParams);\n    }\n\n\n    private ValueAnimator mAnimation;\n    private AnimUpdateListener mAnimUpdateListener;\n    private AnimListenerAdapter mAnimListenerAdapter;\n    private float mFactor; // 进度因子:0-1\n    private boolean mIsRunning;\n    private int mCurX, mCurY, mDst;\n    private int mDuration = 250;\n    private float mDampFactor = 0.6f; // 滑动阻尼系数\n\n    static class AnimListenerAdapter extends AnimatorListenerAdapter {\n        private final WeakReference<SlideLayout> reference;\n\n        AnimListenerAdapter(SlideLayout view) {\n            this.reference = new WeakReference<>(view);\n        }\n\n        @Override\n        public void onAnimationCancel(Animator animation) {\n            if (isFinish()) {\n                return;\n            }\n            SlideLayout view = reference.get();\n            view.mFactor = 1;\n        }\n\n        @Override\n        public void onAnimationEnd(Animator animation) {\n            if (isFinish()) {\n                return;\n            }\n            SlideLayout view = reference.get();\n            view.mFactor = 1;\n        }\n\n        private boolean isFinish() {\n            SlideLayout view = reference.get();\n            if (view == null || view.getContext() == null\n                    || view.getContext() instanceof Activity && ((Activity) view.getContext()).isFinishing()\n                    || !view.mIsRunning) {\n                return true;\n            }\n            return false;\n        }\n    }\n\n    static class AnimUpdateListener implements ValueAnimator.AnimatorUpdateListener {\n        private final WeakReference<SlideLayout> reference;\n\n        AnimUpdateListener(SlideLayout view) {\n            this.reference = new WeakReference<>(view);\n        }\n\n        @Override\n        public void onAnimationUpdate(ValueAnimator animation) {\n            if (isFinish()) {\n                return;\n            }\n            SlideLayout view = reference.get();\n            view.mFactor = (float) animation.getAnimatedValue();\n            if (view.mDst == -1) {\n                float scrollY = view.mCurY - view.mCurY * view.mFactor;\n                view.scrollTo(0, (int) scrollY);\n            } else if (view.mDst == 1) {\n                float scrollX = view.mCurX - view.mCurX * view.mFactor;\n                view.scrollTo((int) scrollX, 0);\n            }\n            view.invalidate();\n        }\n\n        private boolean isFinish() {\n            SlideLayout view = reference.get();\n            if (view == null || view.getContext() == null\n                    || view.getContext() instanceof Activity && ((Activity) view.getContext()).isFinishing()\n                    || !view.mIsRunning) {\n                return true;\n            }\n            return false;\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/slideback/callback/SlideBackCallBack.java",
    "content": "package me.wizos.loread.view.slideback.callback;\n\npublic interface SlideBackCallBack {\n    void onSlideBack();\n//    void onViewSlideUpdate(int offset);\n}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/slideback/callback/SlideCallBack.java",
    "content": "package me.wizos.loread.view.slideback.callback;\n\npublic abstract class SlideCallBack implements SlideBackCallBack {\n    private SlideBackCallBack callBack;\n\n    public SlideCallBack() {\n    }\n\n    public SlideCallBack(SlideBackCallBack callBack) {\n        this.callBack = callBack;\n    }\n\n    @Override\n    public void onSlideBack() {\n        if (null != callBack) {\n            callBack.onSlideBack();\n        }\n    }\n//    @Override\n//    public void onViewSlideUpdate(int offset) {\n//        if (null != callBack) {\n//            callBack.onViewSlideUpdate(offset);\n//        }\n//    }\n\n    /**\n     * 滑动来源： <br>\n     * EDGE_LEFT    左侧侧滑 <br>\n     * EDGE_RIGHT   右侧侧滑 <br>\n     */\n    public abstract void onSlide(int edgeFrom);\n\n    public abstract void onViewSlide(int edgeFrom, int offset);\n}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/slideback/widget/SlideBackIconView.java",
    "content": "package me.wizos.loread.view.slideback.widget;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Path;\nimport android.util.AttributeSet;\nimport android.view.View;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.Nullable;\n\n/**\n * author : ParfoisMeng\n * time   : 2018/12/19\n * desc   : 边缘返回的图标View\n */\npublic class SlideBackIconView extends View {\n    private Path arrowPath; // 路径对象\n    private Paint arrowPaint; // 画笔对象\n    //private Path bgPath;\n    //private Paint bgPaint;\n\n    // @ColorInt\n    // private int backViewColor = Color.BLACK; // 控件背景色\n\n    private float backViewHeight = 0; // 控件高度\n    private float arrowSize = 10; // 箭头图标大小\n    @ColorInt\n    private int arrowColor = Color.WHITE; // 箭头背景色\n    private float maxSlideLength = 0; // 最大拉动距离\n\n    private float slideLength = 0; // 当前拉动距离\n\n    public SlideBackIconView(Context context) {\n        this(context, null);\n    }\n\n    public SlideBackIconView(Context context, @Nullable AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public SlideBackIconView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init();\n    }\n\n    /**\n     * 初始化 路径与画笔\n     * Path & Paint\n     */\n    private void init() {\n//        bgPath = new Path();\n//        bgPaint = new Paint();\n//        bgPaint.setAntiAlias(true);\n//        bgPaint.setStyle(Paint.Style.FILL_AND_STROKE); // 填充内部和描边\n//        bgPaint.setColor(backViewColor);\n//        bgPaint.setStrokeWidth(1); // 画笔宽度\n\n        arrowPath = new Path();\n        arrowPaint = new Paint();\n        arrowPaint.setAntiAlias(true);\n        arrowPaint.setStyle(Paint.Style.STROKE); // 描边\n        arrowPaint.setColor(arrowColor);\n        arrowPaint.setStrokeWidth(8); // 画笔宽度\n        arrowPaint.setStrokeJoin(Paint.Join.ROUND); // * 结合处的样子 ROUND:圆弧\n\n        setAlpha(0);\n    }\n\n\n    /**\n     * 因为过程中会多次绘制，所以要先重置路径再绘制。\n     * 贝塞尔曲线没什么好说的，相关文章有很多。此曲线经我测试比较类似“即刻App”。\n     * <p>\n     * 方便阅读再写一遍，此段代码中的变量定义：\n     * backViewHeight   控件高度\n     * slideLength      当前拉动距离\n     * maxSlideLength   最大拉动距离\n     * arrowSize        箭头图标大小\n     */\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n        // 背景\n//        bgPath.reset(); // 会多次绘制，所以先重置\n//        bgPath.moveTo(0, 0);\n//        bgPath.cubicTo(0, backViewHeight * 2 / 9, slideLength, backViewHeight / 3, slideLength, backViewHeight / 2);\n//        bgPath.cubicTo(slideLength, backViewHeight * 2 / 3, 0, backViewHeight * 7 / 9, 0, backViewHeight);\n//        canvas.drawPath(bgPath, bgPaint); // 根据设置的贝塞尔曲线路径用画笔绘制\n\n        // 箭头是先直线由短变长再折弯变成箭头状的\n        // 依据当前拉动距离和最大拉动距离计算箭头大小值\n        // 大小到一定值后开始折弯，计算箭头角度值\n        float arrowZoom = slideLength / maxSlideLength; // 箭头大小变化率\n        float arrowAngle = arrowZoom < 0.75f ? 0 : (arrowZoom - 0.75f) * 2; // 箭头角度变化率\n        // 箭头\n        arrowPath.reset(); // 先重置\n        // 结合箭头大小值与箭头角度值设置折线路径\n        arrowPath.moveTo(slideLength / 2 + (arrowSize * arrowAngle), backViewHeight / 2 - (arrowZoom * arrowSize));\n        arrowPath.lineTo(slideLength / 2 - (arrowSize * arrowAngle), backViewHeight / 2);\n        arrowPath.lineTo(slideLength / 2 + (arrowSize * arrowAngle), backViewHeight / 2 + (arrowZoom * arrowSize));\n        canvas.drawPath(arrowPath, arrowPaint);\n\n        setAlpha(slideLength / maxSlideLength - 0.2f); // 最多0.8透明度\n    }\n\n    /**\n     * 更新当前拉动距离并重绘\n     *\n     * @param slideLength 当前拉动距离\n     */\n    public void updateSlideLength(float slideLength) {\n        this.slideLength = slideLength;\n        invalidate(); // 会再次调用onDraw\n    }\n\n\n    /**\n     * 设置最大拉动距离\n     *\n     * @param maxSlideLength px值\n     */\n    public void setMaxSlideLength(float maxSlideLength) {\n        this.maxSlideLength = maxSlideLength;\n    }\n\n    /**\n     * 设置箭头图标大小\n     *\n     * @param arrowSize px值\n     */\n    public void setArrowSize(float arrowSize) {\n        this.arrowSize = arrowSize;\n    }\n\n    /**\n     * 箭头颜色\n     */\n    public void setArrowColor(@ColorInt int arrowColor) {\n        if (arrowPaint != null) {\n            arrowPaint.setColor(arrowColor);\n        }\n    }\n\n//    /**\n//     * 设置返回Icon背景色\n//     *\n//     * @param backViewColor ColorInt\n//     */\n//    public void setBackViewColor(@ColorInt int backViewColor) {\n//        this.backViewColor = backViewColor;\n//    }\n\n    /**\n     * 设置返回Icon的高度\n     *\n     * @param backViewHeight px值\n     */\n    public void setBackViewHeight(float backViewHeight) {\n        this.backViewHeight = backViewHeight;\n    }\n\n    public float getBackViewHeight() {\n        return backViewHeight;\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/slideback/widget/SlideBackInterceptLayout.java",
    "content": "package me.wizos.loread.view.slideback.widget;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.widget.FrameLayout;\n\nimport com.socks.library.KLog;\n\nimport java.util.ArrayList;\n\n/**\n * author : ParfoisMeng\n * time   : 2019/01/10\n * desc   : 处理事件拦截的Layout\n */\npublic class SlideBackInterceptLayout extends FrameLayout {\n\n    private float leftSideSlideLength = 0; // 边缘滑动响应距离\n    private float rightSideSlideLength = 0; // 边缘滑动响应距离\n\n    public SlideBackInterceptLayout(Context context) {\n        this(context, null);\n    }\n\n    public SlideBackInterceptLayout(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public SlideBackInterceptLayout(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n    }\n\n    //    @Override\n//    public boolean onInterceptTouchEvent(MotionEvent ev) {\n//        return ev.getAction() == MotionEvent.ACTION_DOWN && (ev.getRawX() <= leftSideSlideLength || ev.getRawX() >= rightSideSlideLength);\n//    }\n    @Override\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n        return ev.getAction() == MotionEvent.ACTION_DOWN && isMotionTrigger(ev);\n    }\n\n    public void setSideSlideLength(float screenWidth, float sideSlideLength) {\n        this.leftSideSlideLength = sideSlideLength;\n        this.rightSideSlideLength = screenWidth - sideSlideLength;\n    }\n\n\n    private ArrayList<float[]> zoneList;\n\n    public void addXTriggerZone(float[] zone) {\n        if (zoneList == null) {\n            zoneList = new ArrayList<>();\n        }\n        float tmp;\n        if (zone[0] > zone[1]) {\n            tmp = zone[0];\n            zone[0] = zone[1];\n            zone[1] = tmp;\n            zoneList.add(zone);\n        } else if (zone[0] < zone[1]) {\n            zoneList.add(zone);\n        }\n    }\n\n    public void setXTriggerZone(float[]... zones) {\n        zoneList = new ArrayList<>(zones.length);\n        float tmp;\n        for (float[] zone : zones) {\n            if (zone[0] > zone[1]) {\n                tmp = zone[0];\n                zone[0] = zone[1];\n                zone[1] = tmp;\n                zoneList.add(zone);\n            } else if (zone[0] < zone[1]) {\n                zoneList.add(zone);\n            }\n        }\n    }\n\n    private boolean isMotionTrigger(MotionEvent ev) {\n        for (float[] zone : zoneList) {\n            if (zone[0] <= ev.getRawX() && ev.getRawX() <= zone[1]) {\n                KLog.e(\"事件成功：\" + zone[0] + \" , \" + ev.getRawX() + \" , \" + zone[1] + \" = \" + ev.getAction());\n                return true;\n            }\n        }\n        KLog.e(\"事件不成功：\" + \" , \" + ev.getRawX() + \" , \" + \" = \" + ev.getAction());\n        return false;\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/webview/DownloadListenerS.java",
    "content": "package me.wizos.loread.view.webview;\n\nimport android.app.Activity;\nimport android.app.DownloadManager;\nimport android.content.ClipData;\nimport android.content.ClipboardManager;\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Environment;\nimport android.text.TextUtils;\nimport android.webkit.DownloadListener;\nimport android.webkit.WebView;\nimport android.widget.EditText;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\n\nimport com.afollestad.materialdialogs.DialogAction;\nimport com.afollestad.materialdialogs.MaterialDialog;\nimport com.hjq.toast.ToastUtils;\nimport com.socks.library.KLog;\n\nimport me.wizos.loread.R;\nimport me.wizos.loread.utils.FileUtil;\nimport me.wizos.loread.utils.Tool;\nimport me.wizos.loread.utils.UriUtil;\n\nimport static android.content.Context.DOWNLOAD_SERVICE;\n\n/**\n * @author by Wizos on 2018/6/20.\n */\n\npublic class DownloadListenerS implements DownloadListener {\n    private Activity context;\n    private EditText fileNameEditor;\n    private WebView webView;\n\n    public DownloadListenerS(Activity context) {\n        this.context = context;\n    }\n\n    public DownloadListenerS setWebView(WebView webView) {\n        this.webView = webView;\n        return this;\n    }\n\n    @Override\n    public void onDownloadStart(final String url, final String userAgent, final String contentDisposition, final String mimeType, final long contentLength) {\n        String neutralText = \"复制下载地址\";\n        if (!TextUtils.isEmpty(mimeType) && webView != null) {\n            if (mimeType.toLowerCase().startsWith(\"video\")) {\n                neutralText = \"播放该视频\";\n            } else if (mimeType.toLowerCase().startsWith(\"audio\")&& webView != null) {\n                neutralText = \"播放该音频\";\n            }\n        }\n\n\n        KLog.e(\"下载\" + url + \" , \" + userAgent + \" , \"  + contentDisposition);\n//        KLog.e(\"下载\", contentDisposition); // attachment; filename=com.android36kr.app_7.4.2_18060821.apk\n        KLog.e(\"下载\" + mimeType); //  application/vnd.android.package-archive\n        KLog.e(\"下载5\", contentLength);\n\n        MaterialDialog downloadDialog = new MaterialDialog.Builder(context)\n                .title(R.string.do_you_want_to_download_files)\n                .customView(R.layout.config_download_view, true)\n                .neutralText(neutralText)\n                .onNeutral(new MaterialDialog.SingleButtonCallback() {\n                    @Override\n                    public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n                        if (!TextUtils.isEmpty(mimeType)) {\n                            if (mimeType.toLowerCase().startsWith(\"video\")&& webView != null) {\n                                playVideo(url);\n                            } else if (mimeType.toLowerCase().startsWith(\"audio\")&& webView != null) {\n                                playAudio(url);\n                            } else {\n                                copyUrl(url);\n                            }\n                        }\n                    }\n                })\n                .negativeText(android.R.string.cancel)\n                .positiveText(R.string.agree)\n                .onPositive(new MaterialDialog.SingleButtonCallback() {\n                    @Override\n                    public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n//                        KLog.e(\"输入框内容的是：\" + fileNameEditor.getText());\n                        downloadBySystem(url, fileNameEditor.getText().toString());\n                    }\n                })\n                .show();\n\n        String fileName = UriUtil.guessFileName(url, contentDisposition, mimeType);\n        String fileSize = Tool.getNetFileSizeDescription(context, contentLength);\n\n        fileNameEditor = (EditText) downloadDialog.findViewById(R.id.file_name_edit);\n        fileNameEditor.setText(fileName);\n\n        TextView fileSizeView = (TextView) downloadDialog.findViewById(R.id.file_size);\n        fileSizeView.setText(context.getString(R.string.file_size, fileSize));\n    }\n\n\n    // 作者：落英坠露 ,链接：https://www.jianshu.com/p/6e38e1ef203a\n    private void downloadBySystem(String url, String fileName) {\n        // 方法1：跳转浏览器下载\n//        final Intent intent = new Intent(Intent.ACTION_VIEW);\n//        intent.addCategory(Intent.CATEGORY_BROWSABLE);\n//        intent.setData(Uri.parse(url));\n//        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n\n        // 方法2、使用系统的下载服务\n        // 指定下载地址\n        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));\n        // 允许媒体扫描，根据下载的文件类型被加入相册、音乐等媒体库\n        request.allowScanningByMediaScanner();\n        // 设置通知的显示类型，下载进行时和完成后显示通知\n        // Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED 表示在下载过程中通知栏会一直显示该下载的Notification，在下载完成后该Notification会继续显示，直到用户点击该Notification或者消除该Notification。\n        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);\n        // 设置通知栏的标题，如果不设置，默认使用文件名\n//        request.setTitle(\"This is title\");\n        // 设置通知栏的描述\n//        request.setDescription(\"This is description\");\n        // 允许该记录在下载管理界面可见\n        request.setVisibleInDownloadsUi(true);\n        // 允许在计费流量下下载\n        request.setAllowedOverMetered(true);\n        // 允许漫游时下载\n        request.setAllowedOverRoaming(true);\n        // 允许下载的网路类型\n        request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI | DownloadManager.Request.NETWORK_MOBILE);\n\n        // 设置下载文件保存的路径和文件名。\n        // Content-disposition 是 MIME 协议的扩展，MIME 协议指示 MIME 用户代理如何显示附加的文件。当 Internet Explorer 接收到头时，它会激活文件下载对话框，它的文件名框自动填充了头中指定的文件名。（请注意，这是设计导致的；无法使用此功能将文档保存到用户的计算机上，而不向用户询问保存位置。）\n//        KLog.e(\"下载\", \"文件名：\" + fileName);\n        request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, FileUtil.getSaveableName(fileName));\n//        另外可选一下方法，自定义下载路径\n//        request.setDestinationUri()\n//        request.setDestinationInExternalFilesDir()\n        DownloadManager downloadManager = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE);\n        // 添加一个下载任务\n        downloadManager.enqueue(request);\n//        KLog.e(\"下载\", \"下载id为：\" + downloadId);\n    }\n\n    private void playVideo(String url) {\n        webView.loadDataWithBaseURL(\n                \"\",\n                \"<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1.0,user-scalable=no'><title>视频</title></head><body><video src='\" + url + \"' preload='metadata' width='100%' height='auto' controls>你的浏览器不支持HTMl5，无法播放该视频</video></body></html>\",\n                \"text/html\",\n                \"UTF-8\",\n                null);\n    }\n\n    private void playAudio(String url) {\n        webView.loadDataWithBaseURL(\n                \"\",\n                \"<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1.0,user-scalable=no'><title>音频</title></head>\" +\n                        \"<body><audio src='\" + url + \"' preload='metadata' width='100%' height='auto' controls>你的浏览器不支持HTMl5，无法播放该音频</audio></body></html>\",\n                \"text/html\",\n                \"UTF-8\",\n                null);\n    }\n\n    private void copyUrl(String url) {\n        // 获取剪贴板管理器\n        ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);\n        // 创建普通字符型ClipData\n        ClipData mClipData = ClipData.newPlainText(\"url\", url);\n        // 将ClipData内容放到系统剪贴板里。\n        cm.setPrimaryClip(mClipData);\n        ToastUtils.show(\"复制成功\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/webview/FastScrollWebView.java",
    "content": "package me.wizos.loread.view.webview;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.webkit.WebView;\n\nimport me.wizos.loread.view.fastscroll.FastScrollDelegate;\nimport me.wizos.loread.view.fastscroll.FastScrollDelegate.FastScrollable;\n\n/**\n * https://github.com/Mixiaoxiao/FastScroll-Everywhere FastScrollWebView\n *\n * @author Mixiaoxiao 2016-08-31\n */\npublic class FastScrollWebView extends WebView implements FastScrollable {\n\n    private FastScrollDelegate mFastScrollDelegate;\n\n    // ===========================================================\n    // Constructors\n    // ===========================================================\n\n    public FastScrollWebView(Context context) {\n        super(context);\n        createFastScrollDelegate(context);\n    }\n\n    public FastScrollWebView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        createFastScrollDelegate(context);\n    }\n\n    public FastScrollWebView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n        createFastScrollDelegate(context);\n    }\n\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    public FastScrollWebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n        super(context, attrs, defStyleAttr, defStyleRes);\n        createFastScrollDelegate(context);\n    }\n\n    // ===========================================================\n    // createFastScrollDelegate\n    // ===========================================================\n\n    private void createFastScrollDelegate(Context context) {\n        mFastScrollDelegate = new FastScrollDelegate.Builder(this).build();\n    }\n\n    // ===========================================================\n    // Delegate\n    // ===========================================================\n\n    @Override\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n//        KLog.e(\"拦截手势操作：\"  );\n        if (mFastScrollDelegate.onInterceptTouchEvent(ev)) {\n//            KLog.e(\"拦截手势操作结果为 true\"  );\n            return true;\n        }\n//        KLog.e(\"拦截手势操作结果为 false\"  );\n        return super.onInterceptTouchEvent(ev);\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        if (mFastScrollDelegate.onTouchEvent(event)) {\n//            KLog.e(\"执行父onTouchEventInternal为   true\"  );\n            return true;\n        }\n//        KLog.e(\"执行父onTouchEventInternal为    false\"  );\n        return super.onTouchEvent(event);\n    }\n\n    @Override\n    protected void onAttachedToWindow() {\n        super.onAttachedToWindow();\n        mFastScrollDelegate.onAttachedToWindow();\n    }\n\n    @Override\n    protected void onVisibilityChanged(View changedView, int visibility) {\n        super.onVisibilityChanged(changedView, visibility);\n        if (mFastScrollDelegate != null) {\n            mFastScrollDelegate.onVisibilityChanged(changedView, visibility);\n        }\n    }\n\n    @Override\n    protected void onWindowVisibilityChanged(int visibility) {\n        super.onWindowVisibilityChanged(visibility);\n        mFastScrollDelegate.onWindowVisibilityChanged(visibility);\n    }\n\n    @Override\n    protected boolean awakenScrollBars() {\n        return mFastScrollDelegate.awakenScrollBars();\n    }\n\n    @Override\n    protected void dispatchDraw(Canvas canvas) {\n        super.dispatchDraw(canvas);\n        mFastScrollDelegate.dispatchDrawOver(canvas);\n    }\n\n    // ===========================================================\n    // FastScrollable IMPL, ViewInternalSuperMethods\n    // ===========================================================\n\n    @Override\n    public void superOnTouchEvent(MotionEvent event) {\n        super.onTouchEvent(event);\n    }\n\n    @Override\n    public int superComputeVerticalScrollExtent() {\n        return super.computeVerticalScrollExtent();\n    }\n\n    @Override\n    public int superComputeVerticalScrollOffset() {\n        return super.computeVerticalScrollOffset();\n    }\n\n    @Override\n    public int superComputeVerticalScrollRange() {\n        return super.computeVerticalScrollRange();\n    }\n\n    @Override\n    public View getFastScrollableView() {\n        return this;\n    }\n\n    /**\n     * @deprecated use {@link #getFastScrollDelegate()} instead\n     */\n    public FastScrollDelegate getDelegate() {\n        return getFastScrollDelegate();\n    }\n\n    @Override\n    public FastScrollDelegate getFastScrollDelegate() {\n        return mFastScrollDelegate;\n    }\n\n    @Override\n    public void setNewFastScrollDelegate(FastScrollDelegate newDelegate) {\n        if (newDelegate == null) {\n            throw new IllegalArgumentException(\"setNewFastScrollDelegate must NOT be NULL.\");\n        }\n        mFastScrollDelegate.onDetachedFromWindow();\n        mFastScrollDelegate = newDelegate;\n        newDelegate.onAttachedToWindow();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/webview/LongClickPopWindow.java",
    "content": "package me.wizos.loread.view.webview;\n\nimport android.content.ClipData;\nimport android.content.ClipboardManager;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.text.TextUtils;\nimport android.view.Gravity;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.webkit.WebView;\nimport android.widget.PopupWindow;\n\nimport com.hjq.toast.ToastUtils;\nimport com.socks.library.KLog;\n\nimport me.wizos.loread.R;\n\n/**\n * @author Wizos on 2018/9/16.\n */\n\npublic class LongClickPopWindow extends PopupWindow {\n    private View webViewLongClickedPopWindow;\n    private Context context;\n    private WebView.HitTestResult result;\n    private WebView webView;\n    private int x, y;\n\n    /**\n     * 构造函数\n     *\n     * @param context 上下文\n     * @param width   宽度\n     * @param height  高度 *\n     */\n    public LongClickPopWindow(Context context, WebView webView, int width, int height, int x, int y) {\n        super(context);\n        if (context == null | webView == null) {\n            return;\n        }\n        this.result = webView.getHitTestResult();\n        if (null == result) {\n            return;\n        }\n        if (result.getType() == WebView.HitTestResult.UNKNOWN_TYPE) {\n            KLog.e(\"长按未知：\" + result.getType() + \" , \" + result.getExtra());\n            return;\n        }\n        this.context = context;\n        this.webView = webView;\n        this.x = x;\n        this.y = y;\n        LayoutInflater itemLongClickedPopWindowInflater = LayoutInflater.from(this.context);\n        this.webViewLongClickedPopWindow = itemLongClickedPopWindowInflater.inflate(R.layout.webview_long_clicked_popwindow, null);\n\n        //设置默认选项\n        setWidth(width);\n        setHeight(height);\n        setContentView(this.webViewLongClickedPopWindow);\n        setOutsideTouchable(true);\n        setFocusable(true);\n\n        //创建\n        initTab();\n//        showAtLocation(webView, Gravity.TOP|Gravity.LEFT, downX, downY + 10);\n    }\n\n    //实例化\n    private void initTab() {\n\n        switch (result.getType()) {\n//            case FAVORITES_ITEM_POPUPWINDOW:\n//                this.itemLongClickedPopWindowView = this.itemLongClickedPopWindowInflater.inflate(R.layout.list_item_longclicked_favorites, null);\n//                break;\n//            case FAVORITES_VIEW_POPUPWINDOW: //对于书签内容弹出菜单，未作处理\n//                break;\n//            case HISTORY_ITEM_POPUPWINDOW:\n//                this.itemLongClickedPopWindowView = this.itemLongClickedPopWindowInflater.inflate(R.layout.list_item_longclicked_history, null);\n//                break;\n//            case HISTORY_VIEW_POPUPWINDOW: //对于历史内容弹出菜单，未作处理\n//                break;\n\n//            case WebView.HitTestResult.EDIT_TEXT_TYPE: // 选中的文字类型\n//            case WebView.HitTestResult.PHONE_TYPE: // 处理拨号\n//            case WebView.HitTestResult.EMAIL_TYPE: // 处理Email\n//            case WebView.HitTestResult.GEO_TYPE: // 　地图类型\n//            case WebView.HitTestResult.SRC_ANCHOR_TYPE: // 超链接\n//            case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE: // 带有链接的图片类型\n//            case WebView.HitTestResult.IMAGE_TYPE: // 处理长按图片的菜单项\n//                String url = result.getExtra();//获取图片\n//                break;\n//            case WebView.HitTestResult.UNKNOWN_TYPE: //未知\n\n\n            case WebView.HitTestResult.SRC_ANCHOR_TYPE://超链接\n                if(TextUtils.isEmpty(result.getExtra())){\n                    return;\n                }\n                KLog.d(\"超链接为：\" + result.getExtra() );\n                this.webViewLongClickedPopWindow.findViewById(R.id.webview_copy_link)\n                        .setOnClickListener(new View.OnClickListener() {\n                            @Override\n                            public void onClick(View v) {\n                                LongClickPopWindow.this.dismiss();\n                                //获取剪贴板管理器：\n                                ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);\n                                // 创建普通字符型ClipData\n                                ClipData mClipData = ClipData.newRawUri(\"url\", Uri.parse(result.getExtra()));\n                                // 将ClipData内容放到系统剪贴板里。\n                                cm.setPrimaryClip(mClipData);\n                                ToastUtils.show(context.getString(R.string.copy_success));\n                            }\n                        });\n\n                this.webViewLongClickedPopWindow.findViewById(R.id.webview_share_link)\n                        .setOnClickListener(new View.OnClickListener() {\n                            @Override\n                            public void onClick(View v) {\n                                LongClickPopWindow.this.dismiss();\n                                Intent sendIntent = new Intent(Intent.ACTION_SEND);\n                                sendIntent.setType(\"text/plain\");\n                                sendIntent.putExtra(Intent.EXTRA_TEXT, result.getExtra());\n//                                sendIntent.setData(Uri.parse(status.getExtra()));\n                                //sendIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n                                context.startActivity(Intent.createChooser(sendIntent, context.getString(R.string.share_to)));\n                            }\n                        });\n\n                this.webViewLongClickedPopWindow.findViewById(R.id.webview_open_mode)\n                        .setOnClickListener(new View.OnClickListener() {\n                            @Override\n                            public void onClick(View v) {\n                                LongClickPopWindow.this.dismiss();\n                                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(result.getExtra()));\n                                // 每次都要选择打开方式\n                                context.startActivity(Intent.createChooser(intent, context.getString(R.string.open_mode)));\n                                //context.startActivity(intent);\n                            }\n                        });\n\n                showAtLocation(webView, Gravity.TOP | Gravity.START, x, y);\n                break;\n            case WebView.HitTestResult.IMAGE_TYPE: //图片\n                // TODO: 2019/5/1 重新下载 , 查看原图\n                break;\n            case WebView.HitTestResult.UNKNOWN_TYPE: //对于历史内容弹出菜单，未作处理\n                break;\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/webview/NestedScrollWebView.java",
    "content": "/*\n * Copyright (C)  LeonDevLifeLog(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage me.wizos.loread.view.webview;\n\nimport android.content.Context;\nimport android.view.MotionEvent;\n\nimport androidx.core.view.NestedScrollingChild;\nimport androidx.core.view.NestedScrollingChildHelper;\nimport androidx.core.view.ViewCompat;\n\n//import com.tencent.smtt.sdk.WebView;\n\n/**\n * 结合CoordinatorLayout可以与Toolbar联动的webview\n *\n * @author LeonDevLifeLog\n * @since 4.0.0\n */\n\npublic class NestedScrollWebView extends FastScrollWebView implements NestedScrollingChild {\n    /***\n     * 方法一\n     * https://github.com/fashare2015/NestedScrollWebView\n     */\n    public NestedScrollWebView(Context context) {\n        super(context);\n        initView();\n        // 判断用户在进行滑动操作的最小距离\n//        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop() + 100;\n//        KLog.e(\"最小滑动距离：\" + mTouchSlop );\n    }\n\n    private static final int INVALID_POINTER = -1;\n\n    private void initView() {\n        mChildHelper = new NestedScrollingChildHelper(this);\n        setNestedScrollingEnabled(true);\n    }\n\n\n    /**\n     * Position of the last motion event.\n     */\n    private int mLastMotionY;\n\n    /**\n     * ID of the active pointer. This is used to retain consistency during\n     * drags/flings if multiple pointers are used.\n     */\n    private int mActivePointerId = INVALID_POINTER;\n\n    /**\n     * Used during scrolling to retrieve the new offset within the window.\n     */\n    private final int[] mScrollOffset = new int[2];\n    private final int[] mScrollConsumed = new int[2];\n    private NestedScrollingChildHelper mChildHelper;\n    boolean mIsBeingDragged;\n\n\n    @Override\n    public boolean onTouchEvent(MotionEvent ev) {\n        final int actionMasked = ev.getActionMasked();\n\n        switch (actionMasked) {\n            case MotionEvent.ACTION_DOWN:\n                mIsBeingDragged = false;\n\n                // Remember where the motion event started\n                mLastMotionY = (int) ev.getY();\n//                downY = (int) ev.getY();\n\n                mActivePointerId = ev.getPointerId(0);\n                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);\n                break;\n\n            case MotionEvent.ACTION_MOVE:\n//                KLog.e(\"移动开始：\");\n                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);\n                if (activePointerIndex == -1) {\n                    break;\n                }\n//                if( !onlyVerticalMove(ev) ){\n//                    break;\n//                }\n\n                final int y = (int) ev.getY(activePointerIndex);\n                int deltaY = mLastMotionY - y;\n                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {\n                    deltaY -= mScrollConsumed[1];\n                }\n                // Scroll to follow the motion event\n                mLastMotionY = y - mScrollOffset[1];\n\n                final int oldY = getScrollY();\n                final int scrolledDeltaY = Math.max(0, oldY + deltaY) - oldY;\n                final int unconsumedY = deltaY - scrolledDeltaY;\n                if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {\n                    mLastMotionY -= mScrollOffset[1];\n                }\n//                KLog.e(\"移动结束：\");\n                break;\n            case MotionEvent.ACTION_UP:\n                mActivePointerId = INVALID_POINTER;\n                endDrag();\n                break;\n            case MotionEvent.ACTION_CANCEL:\n                mActivePointerId = INVALID_POINTER;\n                endDrag();\n                break;\n            case MotionEvent.ACTION_POINTER_DOWN: {\n                final int index = ev.getActionIndex();\n                mLastMotionY = (int) ev.getY(index);\n                mActivePointerId = ev.getPointerId(index);\n                break;\n            }\n            case MotionEvent.ACTION_POINTER_UP:\n                onSecondaryPointerUp(ev);\n                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));\n                break;\n            default:\n                break;\n        }\n        return super.onTouchEvent(ev);\n    }\n\n    private void endDrag() {\n        mIsBeingDragged = false;\n        stopNestedScroll();\n    }\n\n    private void onSecondaryPointerUp(MotionEvent ev) {\n        final int pointerIndex = ev.getActionIndex();\n        final int pointerId = ev.getPointerId(pointerIndex);\n        if (pointerId == mActivePointerId) {\n            // This was our active pointer going up. Choose a new\n            // active pointer and adjust accordingly.\n            // TODO: Make this decision more intelligent.\n            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;\n            mLastMotionY = (int) ev.getY(newPointerIndex);\n            mActivePointerId = ev.getPointerId(newPointerIndex);\n        }\n    }\n\n\n    @Override\n    public void setNestedScrollingEnabled(boolean enabled) {\n        mChildHelper.setNestedScrollingEnabled(enabled);\n    }\n\n    @Override\n    public boolean isNestedScrollingEnabled() {\n        return mChildHelper.isNestedScrollingEnabled();\n    }\n\n    @Override\n    public boolean startNestedScroll(int axes) {\n        return mChildHelper.startNestedScroll(axes);\n    }\n\n    @Override\n    public void stopNestedScroll() {\n        mChildHelper.stopNestedScroll();\n    }\n\n    @Override\n    public boolean hasNestedScrollingParent() {\n        return mChildHelper.hasNestedScrollingParent();\n    }\n\n    @Override\n    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {\n//        KLog.e(\"分配滚动事件：\" + dxConsumed + \"  \" + dyConsumed );\n        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);\n    }\n\n    @Override\n    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {\n        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);\n    }\n\n    @Override\n    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {\n        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);\n    }\n\n    @Override\n    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {\n        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);\n    }\n\n\n    /***\n     * 方法二：但是在滚动滑出/滑入toolbar时，会卡顿一下\n     * https://blog.csdn.net/m5314/article/details/68943869\n     */\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/webview/SlowlyProgressBar.java",
    "content": "package me.wizos.loread.view.webview;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.ObjectAnimator;\nimport android.animation.ValueAnimator;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.animation.DecelerateInterpolator;\nimport android.widget.ProgressBar;\n\nimport com.socks.library.KLog;\n\n/**\n * @author 林冠宏 on 2016/7/11. Wizos on 2018/3/18\n * https://github.com/af913337456/SlowlyProgressBar\n */\n\npublic class SlowlyProgressBar {\n    private ProgressBar progressBar;\n    private boolean isStart = false;\n\n    public SlowlyProgressBar(ProgressBar progressBar) {\n        this.progressBar = progressBar;\n    }\n\n    public void destroy() {\n        try {\n            // removeAllViewsInLayout(); 相比而言, removeAllViews() 也调用了removeAllViewsInLayout(), 但是后面还调用了requestLayout(),这个方法是当View的布局发生改变会调用它来更新当前视图, 移除子View会更加彻底. 所以除非必要, 还是推荐使用removeAllViews()这个方法.\n            ViewGroup parent = (ViewGroup) progressBar.getParent();\n            if (parent != null) {\n                parent.removeView(progressBar);\n            }\n            progressBar = null;\n        } catch (Exception e) {\n            KLog.e(\"报错\");\n            e.printStackTrace();\n        }\n    }\n\n    /**\n     * 在 WebViewClient onPageStarted 调用\n     */\n    public void onProgressStart() {\n        progressBar.setVisibility(View.VISIBLE);\n        progressBar.setAlpha(1.0f);\n    }\n\n    /**\n     * 在 WebChromeClient onProgressChange 调用\n     */\n    public void onProgressChange(int newProgress) {\n        int currentProgress = progressBar.getProgress();\n        newProgress = newProgress > currentProgress ? newProgress : currentProgress;\n//        KLog.e(\"进度\" + newProgress);\n        if (newProgress >= 100 && !isStart) {\n            /** 防止调用多次动画 */\n            isStart = true;\n            progressBar.setProgress(newProgress);\n            /** 开启属性动画让进度条平滑消失*/\n            startDismissAnimation(progressBar.getProgress());\n        } else {\n            /** 开启属性动画让进度条平滑递增 */\n            startProgressAnimation(newProgress, currentProgress);\n        }\n    }\n\n    /**\n     * progressBar 进度缓慢递增，300ms/次\n     */\n    private void startProgressAnimation(int newProgress, int currentProgress) {\n        ObjectAnimator animator = ObjectAnimator.ofInt(progressBar, \"progress\", currentProgress, newProgress);\n        animator.setDuration(300);\n        /* 减速形式的加速器，个人喜好 */\n        animator.setInterpolator(new DecelerateInterpolator());\n        animator.start();\n    }\n\n    private void startProgressAnimation2(int newProgress, int currentProgress) {\n        ObjectAnimator animator = ObjectAnimator.ofInt(progressBar, \"progress\", currentProgress, newProgress);\n        float residue = 100f - currentProgress / 100f;\n        animator.setDuration((long) (residue * 8 * 1000)); // 默认匀速动画最大的时长 8 * 1000\n//        animator.setDuration(300);\n        /* 减速形式的加速器，个人喜好 */\n        animator.setInterpolator(new DecelerateInterpolator());\n//        animator.setInterpolator(new LinearInterpolator());\n        animator.start();\n    }\n\n    private void startDismissAnimation(final int progress) {\n        ObjectAnimator anim = ObjectAnimator.ofFloat(progressBar, \"alpha\", 1.0f, 0.0f);\n        // 动画时长\n        anim.setDuration(1500);\n        // 减速\n        anim.setInterpolator(new DecelerateInterpolator());\n        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n\n            @Override\n            public void onAnimationUpdate(ValueAnimator valueAnimator) {\n                // 0.0f ~ 1.0f\n                float fraction = valueAnimator.getAnimatedFraction();\n                int offset = 100 - progress;\n                progressBar.setProgress((int) (progress + offset * fraction));\n            }\n        });\n\n        anim.addListener(new AnimatorListenerAdapter() {\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                progressBar.setProgress(0);\n                progressBar.setVisibility(View.GONE);\n                isStart = false;\n            }\n        });\n        anim.start();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/view/webview/VideoImpl.java",
    "content": "/*\n * Copyright (C)  Justson(https://github.com/Justson/AgentWeb)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage me.wizos.loread.view.webview;\n\nimport android.app.Activity;\nimport android.content.pm.ActivityInfo;\nimport android.graphics.Color;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.Window;\nimport android.view.WindowManager;\nimport android.webkit.WebChromeClient;\nimport android.webkit.WebView;\nimport android.widget.FrameLayout;\n\nimport androidx.core.util.Pair;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\n/**\n * https://www.jianshu.com/p/ed01d00809f4\n *\n * @author cenxiaozhong\n */\npublic class VideoImpl { // implements IVideo, EventInterceptor\n    private static final String TAG = VideoImpl.class.getSimpleName();\n    private Activity mActivity;\n    private WebView mWebView;\n    private Set<Pair<Integer, Integer>> mFlags = null;\n    private View videoView = null;\n    private ViewGroup videoParentView = null;\n    private WebChromeClient.CustomViewCallback mCallback;\n\n    private boolean isPlaying = false;\n\n\n    public VideoImpl(Activity mActivity, WebView webView) {\n        this.mActivity = mActivity;\n        this.mWebView = webView;\n        mFlags = new HashSet<>();\n    }\n\n\n    //    @Override\n    public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) {\n        Activity mActivity;\n        if ((mActivity = this.mActivity) == null || mActivity.isFinishing()) {\n            return;\n        }\n        // 横屏\n        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);\n\n        Window mWindow = mActivity.getWindow();\n        Pair<Integer, Integer> mPair;\n        // 保存当前屏幕的状态\n        if ((mWindow.getAttributes().flags & WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) == 0) {\n            mPair = new Pair<>(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, 0);\n            mWindow.setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);\n            mFlags.add(mPair);\n        }\n\n        // 开启Window级别的硬件加速\n        if ((mWindow.getAttributes().flags & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) == 0) {\n            mPair = new Pair<>(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, 0);\n            mWindow.setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);\n            mFlags.add(mPair);\n        }\n\n\n        if (videoView != null) {\n            callback.onCustomViewHidden();\n            return;\n        }\n//        KLog.e(\"设置\" + mWebView  + \"   \"  + videoParentView);\n        if (mWebView != null) {\n            mWebView.setVisibility(View.GONE);\n        }\n\n        if (videoParentView == null) {\n            FrameLayout mDecorView = (FrameLayout) mActivity.getWindow().getDecorView();\n            videoParentView = new FrameLayout(mActivity);\n            videoParentView.setBackgroundColor(Color.BLACK);\n            videoParentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN); // 全屏\n            mDecorView.addView(videoParentView);\n        }\n\n//        KLog.e(\"设置\" + mWebView.getVisibility()  + \"   \"  + videoParentView);\n        this.mCallback = callback;\n        this.videoView = view;\n        videoParentView.addView(videoView, WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);\n        videoParentView.setVisibility(View.VISIBLE);\n        isPlaying = true;\n    }\n\n    public boolean isPlaying() {\n        return isPlaying;\n    }\n\n    //    @Override\n    public void onHideCustomView() {\n        if (videoView == null) {\n            return;\n        }\n        if (mActivity != null && mActivity.getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {\n            mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);\n        }\n\n        if (!mFlags.isEmpty()) {\n            for (Pair<Integer, Integer> mPair : mFlags) {\n                mActivity.getWindow().setFlags(mPair.second, mPair.first);\n            }\n            mFlags.clear();\n        }\n\n        videoView.setVisibility(View.GONE);\n        if (videoParentView != null && videoView != null) {\n            videoParentView.removeView(videoView);\n\n        }\n        if (videoParentView != null) {\n            // 状态栏和Activity共存，Activity不全屏显示。也就是应用平常的显示画面\n            videoParentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);\n            videoParentView.setVisibility(View.GONE);\n        }\n\n        if (this.mCallback != null) {\n            mCallback.onCustomViewHidden();\n        }\n        this.videoView = null;\n        if (mWebView != null) {\n            mWebView.setVisibility(View.VISIBLE);\n        }\n        isPlaying = false;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/viewmodel/ArticleViewModel.java",
    "content": "package me.wizos.loread.viewmodel;\n\nimport androidx.lifecycle.LiveData;\nimport androidx.lifecycle.ViewModel;\nimport androidx.paging.DataSource;\nimport androidx.paging.LivePagedListBuilder;\nimport androidx.paging.PagedList;\n\nimport com.socks.library.KLog;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.db.Article;\nimport me.wizos.loread.db.ArticleDao;\nimport me.wizos.loread.db.CoreDB;\n\n//LiveData通常结合ViewModel一起使用。我们知道ViewModel是用来存放数据的，因此我们可以将数据库放在ViewModel中进行实例化。\n// 但数据库在实例化的时候需要Context，而ViewModel不能传入任何带有Context引用的对象，所以应该用它的子类AndroidViewModel，它可以接受Application作为参数，用于数据库的实例化。\npublic class ArticleViewModel extends ViewModel {\n    public LiveData<PagedList<Article>> articles;\n    public LiveData<PagedList<Article>> getArticles(String uid, String streamId, int streamType, int streamStatus){\n        //KLog.i(\"生成 getArticles ：\" + streamId );\n        ArticleDao articleDao = CoreDB.i().articleDao();\n        long timeMillis = System.currentTimeMillis();\n        DataSource.Factory<Integer, Article> articleFactory = null;\n\n        if (streamType == App.TYPE_GROUP ) {\n            if (streamId.contains(App.CATEGORY_ALL)) {\n                if (streamStatus == App.STATUS_STARED) {\n                    articleFactory = articleDao.getStared(uid, timeMillis);\n                } else if (streamStatus == App.STATUS_UNREAD) {\n                    articleFactory = articleDao.getUnread(uid, timeMillis);\n                } else {\n                    articleFactory = articleDao.getAll(uid, timeMillis);\n                }\n            } else if (streamId.contains(App.CATEGORY_UNCATEGORIZED)) {\n                if (streamStatus == App.STATUS_STARED) {\n                    articleFactory = articleDao.getStaredByUncategory(uid, timeMillis);\n                } else if (streamStatus == App.STATUS_UNREAD) {\n                    articleFactory = articleDao.getUnreadByUncategory(uid, timeMillis);\n                } else {\n                    articleFactory = articleDao.getAllByUncategory(uid, timeMillis);\n                }\n            } else {\n                KLog.e(\"获取到的分类：\" + streamId );\n                if (streamStatus == App.STATUS_STARED) {\n                    articleFactory = articleDao.getStaredByCategoryId(uid, streamId, timeMillis);\n                } else if (streamStatus == App.STATUS_UNREAD) {\n                    articleFactory = articleDao.getUnreadByCategoryId(uid, streamId, timeMillis);\n                } else {\n                    articleFactory = articleDao.getAllByCategoryId(uid, streamId, timeMillis);\n                }\n            }\n        } else if (streamType == App.TYPE_FEED ) {\n            if (streamStatus == App.STATUS_STARED) {\n                articleFactory = articleDao.getStaredByFeedId(uid, streamId, timeMillis);\n            } else if (streamStatus == App.STATUS_UNREAD) {\n                articleFactory = articleDao.getUnreadByFeedId(uid, streamId, timeMillis);\n            } else {\n                articleFactory = articleDao.getAllByFeedId(uid, streamId, timeMillis);\n            }\n        }\n        // setPageSize 指定每次分页加载的条目数量\n        assert articleFactory != null;\n        articles = new LivePagedListBuilder<>(articleFactory, new PagedList.Config.Builder()\n                .setInitialLoadSizeHint(30)\n                .setPageSize(30)\n                .setPrefetchDistance(15)\n                .build()\n        ).build();\n        return articles;\n    }\n\n\n//    public LiveData<PagedList<Article>> getArticles2(String uid, String streamId, int streamType, int streamStatus){\n//        //KLog.i(\"生成 getArticles ：\" + streamId );\n//        ArticleDao articleDao = CoreDB.i().articleDao();\n//        long timeMillis = System.currentTimeMillis();\n//        DataSource.Factory<Integer, Article> articleFactory = null;\n//        if(streamStatus == App.STATUS_STARED){\n//            if(streamType == App.TYPE_GROUP){\n//                if (streamId.contains(App.CATEGORY_ALL)) {\n//                    articleFactory = articleDao.getStared(uid, timeMillis);\n//                }else if (streamId.contains(App.CATEGORY_UNCATEGORIZED)) {\n//                    articleFactory = articleDao.getStaredByUnTag(uid, timeMillis);\n//                }else {\n//                    KLog.e(\"加载列表：\" + streamId + \" , \" + timeMillis);\n//                    articleFactory = articleDao.getStaredByTagId(uid, streamId, timeMillis);\n//                }\n//            }else {\n//                articleFactory = articleDao.getStaredByFeedId(uid, streamId, timeMillis);\n//            }\n//        }else if (streamStatus == App.STATUS_UNREAD) {\n//            if(streamType == App.TYPE_GROUP){\n//                if (streamId.contains(App.CATEGORY_ALL)) {\n//                    articleFactory = articleDao.getUnread(uid, timeMillis);\n//                }else if (streamId.contains(App.CATEGORY_UNCATEGORIZED)) {\n//                    articleFactory = articleDao.getUnreadByUncategory(uid, timeMillis);\n//                }else {\n//                    articleFactory = articleDao.getUnreadByCategoryId(uid, streamId, timeMillis);\n//                }\n//            }else {\n//                articleFactory = articleDao.getUnreadByFeedId(uid, streamId, timeMillis);\n//            }\n//        }else {\n//            if(streamType == App.TYPE_GROUP){\n//                if (streamId.contains(App.CATEGORY_ALL)) {\n//                    articleFactory = articleDao.getAll(uid, timeMillis);\n//                }else if (streamId.contains(App.CATEGORY_UNCATEGORIZED)) {\n//                    articleFactory = articleDao.getAllByUncategory(uid, timeMillis);\n//                }else {\n//                    articleFactory = articleDao.getAllByCategoryId(uid, streamId, timeMillis);\n//                }\n//            }else {\n//                articleFactory = articleDao.getAllByFeedId(uid, streamId, timeMillis);\n//            }\n//        }\n//        // setPageSize 指定每次分页加载的条目数量\n//        assert articleFactory != null;\n//        articles = new LivePagedListBuilder<>(articleFactory, new PagedList.Config.Builder()\n//                .setInitialLoadSizeHint(30)\n//                .setPageSize(30)\n//                .setPrefetchDistance(15)\n//                .build()\n//        ).build();\n//\n//        KLog.e(\"加载列表B：\" + articles.getValue());\n//        if( articles.getValue() != null){\n//            KLog.e(\"加载列表C：\" + articles.getValue().size());\n//        }\n//        return articles;\n//    }\n\n    public LiveData<PagedList<Article>> getAllByKeyword(String uid, String keyword){\n        // setPageSize 指定每次分页加载的条目数量\n        articles = new LivePagedListBuilder<>(CoreDB.i().articleDao().getAllByKeyword(uid,\"%\" + keyword + \"%\"), new PagedList.Config.Builder().setPageSize(30).setInitialLoadSizeHint(30).setPrefetchDistance(15).build()).build();\n        return articles;\n    }\n\n//    public Article getItem(int position){\n//        return articles.getValue().get(position);\n////        return listData.get(position);\n//    }\n//    public int getItemCount(){\n//        return articles.getValue().size();\n////        return listData.size();\n//    }\n\n//    public void updateItem(Article article){\n//        listData.update(article);\n//    }\n\n//    private ArticleLazyList listData;\n//    public List<Article> getListData(String uid, String streamId, int streamType, int streamStatus){\n//        KLog.e(\"生成ArticleViewModel 2  ：\" + streamId );\n//        ArticleDao articleDao = CoreDB.i().articleDao();\n//        if (streamId.startsWith(\"user/\") || streamType == App.TYPE_GROUP ) {\n//            if (streamId.contains(App.CATEGORY_ALL)) {\n//                if (streamStatus == App.STATUS_STARED) {\n//                    listData = articleDao.getStared(uid);\n//                } else if (streamStatus == App.STATUS_UNREAD) {\n//                    listData = articleDao.getUnread(uid);\n//                } else {\n//                    listData = articleDao.getAll(uid);\n//                }\n//            } else if (streamId.contains(App.CATEGORY_UNCATEGORIZED)) {\n//                if (streamStatus == App.STATUS_STARED) {\n//                    listData = articleDao.getStaredByUncategory(uid);\n//                } else if (streamStatus == App.STATUS_UNREAD) {\n//                    listData = articleDao.getUnreadByUncategory(uid);\n//                } else {\n//                    listData = articleDao.getAllByUncategory(uid);\n//                }\n//            } else {\n//                // TEST:  测试\n//                //Category theCategory = WithDB.i().getCategoryById(streamId);\n//                KLog.e(\"获取到的分类：\" + streamId );\n//                if (streamStatus == App.STATUS_STARED) {\n//                    listData = articleDao.getStaredByCategoryId(uid, streamId);\n//                } else if (streamStatus == App.STATUS_UNREAD) {\n//                    listData = articleDao.getUnreadByCategoryId(uid, streamId);\n//                } else {\n//                    listData = articleDao.getAllByCategoryId(uid, streamId);\n//                }\n//            }\n//        } else if (streamId.startsWith(\"feed/\") || streamType == App.TYPE_FEED ) {\n//            if (streamStatus == App.STATUS_STARED) {\n//                listData = articleDao.getStaredByFeedId(uid, streamId);\n//            } else if (streamStatus == App.STATUS_UNREAD) {\n//                listData = articleDao.getUnreadByFeedId(uid, streamId);\n//            } else {\n//                listData = articleDao.getAllByFeedId(uid, streamId);\n//            }\n//        }\n//        // setPageSize 指定每次分页加载的条目数量\n//        return listData;\n//    }\n\n\n//    public List<Article> getListData2(String uid, String streamId, int streamType, int streamStatus){\n//        ArticleDao articleDao = CoreDB.i().articleDao();\n//        boolean includeValueless = App.i().getGlobalKV().getBoolean(\"including_valueless\",false);\n//        KLog.e(\"获取到的分类：\" + streamId );\n//        if (streamId.startsWith(\"user/\") || streamType == App.TYPE_GROUP ) {\n//            if (streamId.contains(App.CATEGORY_ALL)) {\n//                if (includeValueless) {\n//                    listData = articleDao.getAll(uid);\n//                } else {\n//                    listData = articleDao.getValuable(uid);\n//                }\n//            } else if (streamId.contains(App.CATEGORY_UNCATEGORIZED)) {\n//                if (includeValueless) {\n//                    listData = articleDao.getAllByUncategory(uid);\n//                } else {\n//                    listData = articleDao.getValuableByUnCategory(uid);\n//                }\n//            } else {\n//                if (includeValueless) {\n//                    listData = articleDao.getAllByCategoryId(uid, streamId);\n//                } else {\n//                    listData = articleDao.getValuableByCategoryId(uid, streamId);\n//                }\n//            }\n//        } else if (streamId.startsWith(\"feed/\") || streamType == App.TYPE_FEED ) {\n//            if (includeValueless) {\n//                listData = articleDao.getAllByFeedId(uid, streamId);\n//            } else {\n//                listData = articleDao.getValuableByFeedId(uid, streamId);\n//            }\n//        }\n//        // setPageSize 指定每次分页加载的条目数量\n//        return listData;\n//    }\n\n//    public List<Article> getListData3(String uid, String streamId, int streamType, int streamStatus){\n//        ArticleDao articleDao = CoreDB.i().articleDao();\n//        Cursor cursor = null;\n//        if (streamId.startsWith(\"user/\") || streamType == App.TYPE_GROUP ) {\n//            if (streamId.contains(App.CATEGORY_ALL)) {\n//                if (streamStatus == App.STATUS_STARED) {\n//                    cursor = articleDao.getStared(uid);\n//                } else if (streamStatus == App.STATUS_UNREAD) {\n//                    cursor = articleDao.getUnread(uid);\n//                } else {\n//                    cursor = articleDao.getAll(uid);\n//                }\n//            } else if (streamId.contains(App.CATEGORY_UNCATEGORIZED)) {\n//                if (streamStatus == App.STATUS_STARED) {\n//                    cursor = articleDao.getStaredByUncategory(uid);\n//                } else if (streamStatus == App.STATUS_UNREAD) {\n//                    cursor = articleDao.getUnreadByUncategory(uid);\n//                } else {\n//                    cursor = articleDao.getAllByUncategory(uid);\n//                }\n//            } else {\n//                // TEST:  测试\n//                //Category theCategory = WithDB.i().getCategoryById(streamId);\n//                KLog.e(\"获取到的分类：\" + streamId );\n//                if (streamStatus == App.STATUS_STARED) {\n//                    cursor = articleDao.getStaredByCategoryId(uid, streamId);\n//                } else if (streamStatus == App.STATUS_UNREAD) {\n//                    cursor = articleDao.getUnreadByCategoryId(uid, streamId);\n//                } else {\n//                    cursor = articleDao.getAllByCategoryId(uid, streamId);\n//                }\n//            }\n//        } else if (streamId.startsWith(\"feed/\") || streamType == App.TYPE_FEED ) {\n//            if (streamStatus == App.STATUS_STARED) {\n//                cursor = articleDao.getStaredByFeedId(uid, streamId);\n//            } else if (streamStatus == App.STATUS_UNREAD) {\n//                cursor = articleDao.getUnreadByFeedId(uid, streamId);\n//            } else {\n//                cursor = articleDao.getAllByFeedId(uid, streamId);\n//            }\n//        }\n//        if(listData != null){\n//            ((ArticleLazyList)listData).close();\n//        }\n//        listData = new ArticleLazyList(cursor,true);\n//        AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {\n//            @Override\n//            public void run() {\n//                ((ArticleLazyList)listData).loadRemaining();\n//            }\n//        });\n//        return listData;\n//    }\n//\n//    public List<Article> getAllByKeyword(String uid, String keyword){\n//        if(listData != null){\n//            ((ArticleLazyList)listData).close();\n//        }\n//        listData = new ArticleLazyList(CoreDB.i().articleDao().getAllByKeyword(uid,\"%\" + keyword + \"%\"),true);\n//        AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {\n//            @Override\n//            public void run() {\n//                ((ArticleLazyList)listData).loadRemaining();\n//            }\n//        });\n//        return listData;\n//    }\n\n\n//    public LiveData<PagedList<Article>> getArticles2(String uid, String streamId, int streamType, int streamStatus){\n//        final long timeMillis = System.currentTimeMillis();\n//        KLog.e(\"获取文章时间为：\" + timeMillis );\n//        articles = new LivePagedListBuilder<>(CoreDB.i().articleDao().getUnread(uid,timeMillis), new PagedList.Config.Builder().setPageSize(10).setInitialLoadSizeHint(10).setPrefetchDistance(5).build()).build();\n//        return articles;\n//    }\n\n//    public LiveData<PagedList<Article>> getArticles(){\n//        return articles;\n//    }\n//    public void queryArticles(String uid, String streamId, int streamType, int streamStatus){\n//        ArticleDao articleDao = CoreDB.i().articleDao();\n//        final long timeMillis = System.currentTimeMillis();\n//        DataSource.Factory<Integer, Article> articleFactory = null;\n//\n//        if (streamId.startsWith(\"user/\") || streamType == App.TYPE_GROUP ) {\n//            if (streamId.contains(App.CATEGORY_ALL)) {\n//                if (streamStatus == App.STATUS_STARED) {\n//                    articleFactory = articleDao.getStared(uid, timeMillis);\n//                } else if (streamStatus == App.STATUS_UNREAD) {\n//                    articleFactory = articleDao.getUnread(uid, timeMillis);\n//                } else {\n//                    articleFactory = articleDao.getAll(uid);\n//                }\n//            } else if (streamId.contains(App.CATEGORY_UNCATEGORIZED)) {\n//                if (streamStatus == App.STATUS_STARED) {\n//                    articleFactory = articleDao.getStaredByUncategory(uid, timeMillis);\n//                } else if (streamStatus == App.STATUS_UNREAD) {\n//                    articleFactory = articleDao.getUnreadByUncategory(uid, timeMillis);\n//                } else {\n//                    articleFactory = articleDao.getAllByUncategory(uid);\n//                }\n//            } else {\n//                // TEST:  测试\n//                //Category theCategory = WithDB.i().getCategoryById(streamId);\n//                KLog.e(\"获取到的分类：\" + streamId );\n//                if (streamStatus == App.STATUS_STARED) {\n//                    articleFactory = articleDao.getStaredByCategoryId(uid, streamId, timeMillis);\n//                } else if (streamStatus == App.STATUS_UNREAD) {\n//                    articleFactory = articleDao.getUnreadByCategoryId(uid, streamId, timeMillis);\n//                } else {\n//                    articleFactory = articleDao.getAllByCategoryId(uid, streamId);\n//                }\n//            }\n//        } else if (streamId.startsWith(\"feed/\") || streamType == App.TYPE_FEED ) {\n//            if (streamStatus == App.STATUS_STARED) {\n//                articleFactory = articleDao.getStaredByFeedId(uid, streamId, timeMillis);\n//            } else if (streamStatus == App.STATUS_UNREAD) {\n//                articleFactory = articleDao.getUnreadByFeedId(uid, streamId, timeMillis);\n//            } else {\n//                articleFactory = articleDao.getAllByFeedId(uid, streamId);\n//            }\n//        }\n//        // setPageSize 指定每次分页加载的条目数量\n//        articles = new LivePagedListBuilder<>(articleFactory, new PagedList.Config.Builder().setPageSize(30).setInitialLoadSizeHint(10).setPrefetchDistance(5).build()).build();\n//    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/viewmodel/InoReaderUserViewModel.java",
    "content": "package me.wizos.loread.viewmodel;\n\nimport android.app.Application;\nimport android.text.TextUtils;\nimport android.util.Patterns;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.AndroidViewModel;\nimport androidx.lifecycle.LiveData;\nimport androidx.lifecycle.MutableLiveData;\n\nimport com.socks.library.KLog;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.Contract;\nimport me.wizos.loread.R;\nimport me.wizos.loread.activity.login.LoginFormState;\nimport me.wizos.loread.activity.login.LoginResult;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.User;\nimport me.wizos.loread.db.UserDao;\nimport me.wizos.loread.network.api.InoReaderApi;\nimport me.wizos.loread.network.callback.CallbackX;\n\n// LiveData通常结合ViewModel一起使用。我们知道ViewModel是用来存放数据的，因此我们可以将数据库放在ViewModel中进行实例化。\n// 但数据库在实例化的时候需要Context，而ViewModel不能传入任何带有Context引用的对象，所以应该用它的子类AndroidViewModel，它可以接受Application作为参数，用于数据库的实例化。\npublic class InoReaderUserViewModel extends AndroidViewModel {\n    private UserDao userDao;\n    // Creates a PagedList object with 50 items per page.\n    public InoReaderUserViewModel(@NonNull Application application) {\n        super(application);\n        this.userDao = CoreDB.i().userDao();\n    }\n\n\n    private MutableLiveData<LoginFormState> loginFormLiveData = new MutableLiveData<>();\n    private MutableLiveData<LoginResult> loginResultLiveData = new MutableLiveData<>();\n\n    public LiveData<LoginFormState> getLoginFormLiveData() {\n        return loginFormLiveData;\n    }\n\n    public LiveData<LoginResult> getLoginResult() {\n        return loginResultLiveData;\n    }\n\n    public void login(String host, String username, String password) {\n        InoReaderApi inoReaderApi = new InoReaderApi(host);\n\n        inoReaderApi.login(username, password, new CallbackX<String,String>() {\n            @Override\n            public void onSuccess(String auth) {\n                User user = new User();\n                user.setSource(Contract.PROVIDER_INOREADER);\n                user.setId(Contract.PROVIDER_INOREADER + \"_\" + username);\n                user.setUserId(username);\n                user.setUserName(username);\n                user.setUserPassword(password);\n                user.setAuth(auth);\n                user.setExpiresTimestamp(0);\n                user.setHost(host);\n                inoReaderApi.setAuthorization(auth);\n                App.i().getKeyValue().putString(Contract.UID, user.getId());\n                KLog.i(\"登录成功：\" + user.getId());\n                User userTmp = userDao.getById(user.getId());\n                if (userTmp != null) {\n                    CoreDB.i().userDao().update(user);\n                }else {\n                    CoreDB.i().userDao().insert(user);\n                }\n\n                LoginResult loginResult = new LoginResult().setSuccess(true).setData(auth);\n                loginResultLiveData.postValue(loginResult);\n            }\n\n            @Override\n            public void onFailure(String error) {\n                LoginResult loginResult = new LoginResult().setSuccess(false).setData(App.i().getString(R.string.login_failed_reason, error));\n                loginResultLiveData.postValue(loginResult);\n            }\n        });\n    }\n\n    public void loginDataChanged(String host, String username, String password) {\n        LoginFormState loginFormState = new LoginFormState();\n\n        if (!isHostValid(host)) {\n            loginFormState.setHostHint(R.string.invalid_host);\n        } else if (!isUserNameValid(username)) {\n            loginFormState.setUsernameHint(R.string.invalid_username);\n        } else if (!isPasswordValid(password)) {\n            loginFormState.setPasswordHint(R.string.invalid_password);\n        } else {\n            loginFormState.setDataValid(true);\n        }\n\n        loginFormLiveData.setValue(loginFormState);\n    }\n\n    private boolean isHostValid(String host) {\n        return !TextUtils.isEmpty(host) && Patterns.WEB_URL.matcher(host).matches();\n    }\n\n    // A placeholder username validation check\n    private boolean isUserNameValid(String username) {\n        return !TextUtils.isEmpty(username) && !username.trim().isEmpty();\n    }\n\n    // A placeholder password validation check\n    private boolean isPasswordValid(String password) {\n        return !TextUtils.isEmpty(password) && password.trim().length() > 5;\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/wizos/loread/viewmodel/TinyRSSUserViewModel.java",
    "content": "package me.wizos.loread.viewmodel;\n\nimport android.app.Application;\nimport android.text.TextUtils;\nimport android.util.Patterns;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.AndroidViewModel;\nimport androidx.lifecycle.LiveData;\nimport androidx.lifecycle.MutableLiveData;\n\nimport com.socks.library.KLog;\n\nimport me.wizos.loread.App;\nimport me.wizos.loread.Contract;\nimport me.wizos.loread.R;\nimport me.wizos.loread.activity.login.LoginFormState;\nimport me.wizos.loread.activity.login.LoginResult;\nimport me.wizos.loread.db.CoreDB;\nimport me.wizos.loread.db.User;\nimport me.wizos.loread.db.UserDao;\nimport me.wizos.loread.network.api.TinyRSSApi;\nimport me.wizos.loread.network.callback.CallbackX;\n\n// LiveData通常结合ViewModel一起使用。我们知道ViewModel是用来存放数据的，因此我们可以将数据库放在ViewModel中进行实例化。\n// 但数据库在实例化的时候需要Context，而ViewModel不能传入任何带有Context引用的对象，所以应该用它的子类AndroidViewModel，它可以接受Application作为参数，用于数据库的实例化。\npublic class TinyRSSUserViewModel extends AndroidViewModel {\n    private UserDao userDao;\n    // Creates a PagedList object with 50 items per page.\n    public TinyRSSUserViewModel(@NonNull Application application) {\n        super(application);\n        this.userDao = CoreDB.i().userDao();\n    }\n\n\n    private MutableLiveData<LoginFormState> loginFormLiveData = new MutableLiveData<>();\n    private MutableLiveData<LoginResult> loginResultLiveData = new MutableLiveData<>();\n\n    public LiveData<LoginFormState> getLoginFormLiveData() {\n        return loginFormLiveData;\n    }\n\n    public LiveData<LoginResult> getLoginResult() {\n        return loginResultLiveData;\n    }\n\n    public void login(String host, String username, String password) {\n        TinyRSSApi tinyRSSApi = new TinyRSSApi(host);\n\n        tinyRSSApi.login(username, password, new CallbackX<String,String>() {\n            @Override\n            public void onSuccess(String auth) {\n                User user = new User();\n                user.setSource(Contract.PROVIDER_TINYRSS);\n                user.setId(Contract.PROVIDER_TINYRSS + \"_\" + username);\n                user.setUserId(username);\n                user.setUserName(username);\n                user.setUserPassword(password);\n                user.setAuth(auth);\n                user.setExpiresTimestamp(0);\n                user.setHost(host);\n                tinyRSSApi.setAuthorization(auth);\n                App.i().getKeyValue().putString(Contract.UID, user.getId());\n                KLog.i(\"登录成功：\" + user.getId());\n                User userTmp = userDao.getById(user.getId());\n                if (userTmp != null) {\n                    CoreDB.i().userDao().update(user);\n                }else {\n                    CoreDB.i().userDao().insert(user);\n                }\n\n                LoginResult loginResult = new LoginResult().setSuccess(true).setData(auth);\n                loginResultLiveData.postValue(loginResult);\n            }\n\n            @Override\n            public void onFailure(String error) {\n                LoginResult loginResult = new LoginResult().setSuccess(false).setData(App.i().getString(R.string.login_failed_reason, error));\n                loginResultLiveData.postValue(loginResult);\n            }\n        });\n    }\n\n    public void loginDataChanged(String host, String username, String password) {\n        LoginFormState loginFormState = new LoginFormState();\n\n        if (!isHostValid(host)) {\n            loginFormState.setHostHint(R.string.invalid_host);\n        } else if (!isUserNameValid(username)) {\n            loginFormState.setUsernameHint(R.string.invalid_username);\n        } else if (!isPasswordValid(password)) {\n            loginFormState.setPasswordHint(R.string.invalid_password);\n        } else {\n            loginFormState.setDataValid(true);\n        }\n\n        loginFormLiveData.setValue(loginFormState);\n    }\n\n    private boolean isHostValid(String host) {\n        return !TextUtils.isEmpty(host) && Patterns.WEB_URL.matcher(host).matches();\n    }\n\n    // A placeholder username validation check\n    private boolean isUserNameValid(String username) {\n        return !TextUtils.isEmpty(username) && !username.trim().isEmpty();\n    }\n\n    // A placeholder password validation check\n    private boolean isPasswordValid(String password) {\n        return !TextUtils.isEmpty(password) && password.trim().length() > 5;\n    }\n}\n"
  },
  {
    "path": "app/src/main/res/anim/fade_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:interpolator=\"@android:anim/decelerate_interpolator\">\n\n    <alpha\n        android:fromAlpha=\"0.0\"\n        android:toAlpha=\"1.0\"\n        android:duration=\"150\" />\n</set>"
  },
  {
    "path": "app/src/main/res/anim/fade_out.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:interpolator=\"@android:anim/accelerate_interpolator\">\n    <alpha\n        android:fromAlpha=\"1.0\"\n        android:toAlpha=\"0.0\"\n        android:duration=\"150\" />\n    <!--<scale android:fromXScale=\"1.0\" android:toXScale=\".5\"-->\n    <!--android:fromYScale=\"1.0\" android:toYScale=\".5\"-->\n    <!--android:pivotX=\"50%p\" android:pivotY=\"50%p\"-->\n    <!--android:duration=\"@android:integer/config_mediumAnimTime\" />-->\n</set>"
  },
  {
    "path": "app/src/main/res/anim/in_from_bottom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:interpolator=\"@android:anim/decelerate_interpolator\">\n    <translate\n        android:duration=\"150\"\n        android:fromYDelta=\"100%p\"\n        android:toYDelta=\"0%p\" />\n</set>"
  },
  {
    "path": "app/src/main/res/anim/out_from_bottom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:interpolator=\"@android:anim/accelerate_interpolator\">\n    <translate\n        android:duration=\"150\"\n        android:fromYDelta=\"0%p\"\n        android:toYDelta=\"100%p\" />\n</set>"
  },
  {
    "path": "app/src/main/res/drawable/corners_bg_checked.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <corners android:topLeftRadius=\"20dp\"\n        android:topRightRadius=\"20dp\"\n        android:bottomRightRadius=\"20dp\"\n        android:bottomLeftRadius=\"20dp\"/>\n    <stroke android:width=\"1dp\" android:color=\"#28A4E9\" />\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/corners_bg_uncheck.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <corners\n        android:radius=\"20dp\"\n        android:background=\"#FBFBFB\"/>\n    <stroke android:width=\"1dp\" android:color=\"#D0D0D0\" />\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/custom_progress_bar_thumb.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item android:drawable=\"@drawable/custom_thumb_src\" android:state_enabled=\"false\"/>\n    <item android:drawable=\"@drawable/custom_thumb_src\" android:state_pressed=\"true\"/>\n    <item android:drawable=\"@drawable/custom_thumb_src\" android:state_selected=\"true\"/>\n    <item android:drawable=\"@drawable/custom_thumb_src\"/>\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/custom_thumb_src.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\">\n\n    <solid android:color=\"#DEDEDE\" />\n\n    <size\n        android:width=\"9dp\"\n        android:height=\"9dp\" />\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/flyme_style_switch_button_rectangle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:state_checked=\"true\" android:state_enabled=\"false\">\n        <shape android:shape=\"oval\">\n            <solid android:color=\"#B8E2F8\" />\n        </shape>\n    </item>\n    <item android:state_enabled=\"false\">\n        <layer-list>\n            <item android:bottom=\"6dp\" android:left=\"4dp\" android:right=\"4dp\" android:top=\"6dp\">\n                <shape>\n                    <solid android:color=\"#DEDEDE\" />\n                    <corners android:radius=\"1dp\" />\n                </shape>\n            </item>\n        </layer-list>\n    </item>\n    <item android:state_checked=\"true\">\n        <shape android:shape=\"oval\">\n            <solid android:color=\"#28A4E9\" />\n        </shape>\n    </item>\n    <item>\n        <layer-list>\n            <item android:bottom=\"6dp\" android:left=\"4dp\" android:right=\"4dp\" android:top=\"6dp\">\n                <shape>\n                    <solid android:color=\"#D0D0D0\" />\n                    <corners android:radius=\"1dp\" />\n                </shape>\n            </item>\n        </layer-list>\n    </item>\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/flyme_style_switch_button_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:state_enabled=\"false\">\n        <shape>\n            <solid android:color=\"#FBFBFB\" />\n            <corners android:radius=\"99dp\" />\n            <stroke android:width=\"1dp\" android:color=\"#DEDEDE\" />\n        </shape>\n    </item>\n    <item>\n        <shape>\n            <solid android:color=\"#FBFBFB\" />\n            <corners android:radius=\"99dp\" />\n            <stroke android:width=\"1dp\" android:color=\"#D0D0D0\" />\n        </shape>\n    </item>\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_arrow_auto_mark_readed.xml",
    "content": "<vector android:width=\"12dp\"\n    android:height=\"12dp\"\n    android:viewportHeight=\"1024.0\"\n    android:viewportWidth=\"1024.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"?attr/topbar_fg\"\n        android:pathData=\"M558.1,911.9l367.8,-740.3c2.5,-4.9 4,-10.6 4,-16.7l0,-3.2 -0.1,-0.9 0,-0.7 -0.1,-1.3 0,-0.4 -0.6,-1.7c-0.8,-4.5 -2.6,-8.5 -5,-12l1,1.3c-2.7,-5.2 -6.6,-9.5 -11.2,-12.6l6.5,5.8 -1.3,-1.3 -0.3,-0.4 -1,-1 -0.6,-0.4c-0.7,-0.7 -1.6,-1.3 -2.5,-1.6l0.8,0.1 -0.7,-0.6 -1,-0.7 -0.6,-0.4 -1.3,-1 -0.3,-0.1c-1.2,-0.9 -2.7,-1.5 -4.3,-1.8l2.5,0.8c-3.8,-2.5 -8.4,-4.1 -13.4,-4.6l4.1,0.6c-3.5,-1.2 -7.5,-1.9 -11.7,-1.9l-5.1,0.1 -1.6,0.1 -0.1,0c-6.6,1 -12.5,3.3 -17.6,6.7l0.8,-0.2 -344.7,218.7 -348.2,-220.6c-1.9,-1.2 -4.1,-2.1 -6.5,-2.5l2.9,0.8 -2.6,-0.9 -0.9,-0.4 -2.7,-0.7 -1,-0.1 -2.4,-0.6 -0.7,0c-1,-0.2 -2.1,-0.3 -3.3,-0.3l-3.5,0c-3.3,0 -6.7,0.4 -10,1.4l-0.4,-0c-3.2,1.1 -6.8,1.8 -10.7,2l7.1,-0.8c-4.1,1.4 -7.7,3.5 -10.6,6.3l4.3,-3.5c-5.8,3.3 -10.7,7.4 -14.7,12.4l10.7,-9.8 -1.6,1.1 -2.8,2.3 -0.1,0.3 -1.7,1.6c0,0.4 -0.1,0.6 -0.4,0.9 -1.9,1.9 -3.3,4.3 -4.2,6.9l2,-4.5 -0.7,1 -0.4,0.6 -0.3,0.9 -1.4,2.3 0,0.1c-0.8,1.1 -1.4,2.4 -1.6,3.8l0.2,-1.2c-1.5,3.1 -2.5,6.6 -2.8,10.4 0,-2.7 -0.4,-0.1 -0.4,2.5l-0.1,-1.3 -0.1,2.7 0,0.7 0.1,2 0,0.7 0.1,2.3 0.1,1.1 0.4,2.1 0.1,1c0.3,0.9 0.6,1.4 0.6,2.1l0.3,0.7c0.2,1.2 0.6,2.2 1.3,3.1l-0.3,-0.2c2.6,5.2 4.4,11.3 5.2,17.6l363.9,725.3c7,13.6 21,22.8 37,22.8 0.2,0 0.4,-0 0.5,-0 0.2,0 0.5,0 0.7,0 16,0 29.8,-9.1 36.7,-22.4z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_arrow_right.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_browser.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:width=\"24dp\"\n    android:viewportHeight=\"1024.0\"\n    android:viewportWidth=\"1024.0\">\n    <path\n        android:fillColor=\"?attr/topbar_fg\"\n        android:pathData=\"M512,853.3l147.6,-256h-0.4c14.5,-25.6 23.5,-54.2 23.5,-85.3 0,-51.2 -23,-96.9 -58.9,-128h204.4c16.2,39.7 25.2,82.8 25.2,128a341.3,341.3 0,0 1,-341.3 341.3m-341.3,-341.3c0,-62.3 16.6,-120.3 45.7,-170.7l148.1,256h0.4c29.4,50.8 83.2,85.3 147.2,85.3 19.2,0 37.5,-3.8 55,-9.8l-102.4,176.6C298.7,826.5 170.7,684.4 170.7,512m469.3,0a128,128 0,0 1,-128 128,128 128,0 0,1 -128,-128 128,128 0,0 1,128 -128,128 128,0 0,1 128,128m-128,-341.3a340.5,340.5 0,0 1,295.3 170.7H512c-82.8,0 -151.5,58.9 -167.3,137L243.2,302.1A340.5,340.5 0,0 1,512 170.7m0,-85.3A426.7,426.7 0,0 0,85.3 512a426.7,426.7 0,0 0,426.7 426.7,426.7 426.7,0 0,0 426.7,-426.7A426.7,426.7 0,0 0,512 85.3z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_close.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"?attr/lv_item_desc_color\"\n        android:pathData=\"M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_copy_link.xml",
    "content": "<vector android:height=\"24dp\"\n    android:viewportHeight=\"1024.0\"\n    android:viewportWidth=\"1024.0\"\n    android:width=\"24dp\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"?attr/topbar_fg\"\n        android:pathData=\"M512,20C241.4,20 20,241.4 20,512s221.4,492 492,492 492,-221.4 492,-492S782.6,20 512,20zM512,717.1l-50.1,57.6c-42.7,43.2 -113.8,43.2 -156.5,0l-56.9,-57.6c-42.7,-43.2 -42.7,-115.2 0,-158.4l56.9,-50.7c35.3,-35.7 85.4,-43.2 128,-21.9l-128,122.1c-14.2,14.4 -14.2,35.7 0,50.7l50.1,50.7c14.2,14.4 35.3,14.4 50.1,0l128,-122.1c21.1,43.2 13.7,93.8 -21.6,129.6zM568.9,400.3c14.2,-14.4 35.3,-14.4 50.1,0 14.8,14.4 14.2,35.7 0,50.7L462.5,609.3c-14.2,14.4 -35.3,14.4 -50.1,0 -14.8,-14.4 -14.2,-35.7 0,-50.7l156.5,-158.3zM774.9,450.9L718,508.5c-35.3,35.7 -85.4,43.2 -128,21.9l128,-122.1c14.2,-14.4 14.2,-35.7 0,-50.7l-50.1,-50.7c-14.2,-14.4 -35.3,-14.4 -50.1,0l-128,115.2c-21.6,-35.7 -14.2,-86.4 21.6,-122.1l56.9,-50.7c42.7,-43.2 113.8,-43.2 156.5,0l50.1,50.7c43.3,35.7 43.3,107.7 0,150.9z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_eye.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"?attr/lv_item_desc_color\"\n        android:pathData=\"M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_favor.xml",
    "content": "<vector android:height=\"24dp\" android:viewportHeight=\"1024.0\"\n    android:viewportWidth=\"1024.0\" android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#5a8fca\"\n        android:pathData=\"M767.1,959.9c-5.3,0 -10.7,-1.3 -15.5,-4l-241.3,-133.9 -241.8,133c-10.7,5.9 -23.9,5.2 -33.9,-1.9 -10,-7.1 -15.1,-19.3 -13.1,-31.3l46.9,-284.7 -196.4,-202.1c-8.3,-8.5 -11.2,-20.9 -7.5,-32.2 3.7,-11.3 13.3,-19.6 25,-21.6l155.1,-26.6c17.6,-2.9 34,8.7 37,26.1 3,17.4 -8.7,34 -26.1,37l-95.2,16.3 165.3,170.1c7.1,7.3 10.3,17.5 8.6,27.5l-38.8,235.7 199.6,-109.8c9.6,-5.3 21.3,-5.3 30.9,0.1l199.2,110.5 -38,-235.8c-1.6,-10 1.6,-20.2 8.7,-27.5l164.7,-168.3 -225.7,-34.8c-10.6,-1.6 -19.6,-8.4 -24.1,-18.1l-99.2,-212.4 -100.1,211.8c-7.6,16 -26.6,22.8 -42.6,15.3 -16,-7.6 -22.8,-26.6 -15.3,-42.6l129.2,-273.4c5.3,-11.2 16.6,-18.3 28.9,-18.3 0,0 0,0 0.1,0 12.4,0 23.7,7.2 28.9,18.5l120.8,258.6 270.3,41.7c11.9,1.8 21.7,10.1 25.5,21.5 3.8,11.4 0.9,23.9 -7.5,32.5l-196.9,201.2 45.9,284.9c2,12.1 -3.2,24.3 -13.2,31.3C780,958 773.6,959.9 767.1,959.9z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_favor_fill.xml",
    "content": "<vector android:height=\"24dp\" android:viewportHeight=\"1024.0\"\n    android:viewportWidth=\"1024.0\" android:width=\"24dp\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#5a8fca\"\n        android:pathData=\"M957.2,404.3c-3.8,-11.4 -13.6,-19.7 -25.5,-21.5l-270.3,-41.7 -120.8,-258.6C535.3,71.2 524,64 511.6,64c0,0 -0,0 -0.1,0 -12.4,0 -23.6,7.1 -28.9,18.3l-121.9,258 -270.7,40.8c-11.9,1.8 -21.7,10 -25.6,21.4 -3.8,11.4 -1,23.9 7.4,32.5l196.4,202.1L221.4,922c-2,12.1 3.1,24.3 13.1,31.3 10,7.1 23.2,7.8 33.9,1.9l241.8,-133 241.3,133.9C756.4,958.7 761.8,960 767.1,960c0.3,0 0.5,0 0.6,0 17.7,0 32,-14.3 32,-32 0,-4 -0.7,-7.8 -2,-11.3l-44.9,-278.7 196.9,-201.2C958.1,428.2 961,415.7 957.2,404.3z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_mark_down.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\">\n    <path\n        android:fillColor=\"@color/main_day_light\"\n        android:pathData=\"M7.41,7.84L12,12.42l4.59,-4.58L18,9.25l-6,6 -6,-6z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_mark_unread.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@color/main_day_light\"\n        android:pathData=\"M12,2C6.47,2,2,6.47,2,12c0,5.529,4.47,10,10,10c5.529,0,10-4.471,10-10C22,6.47,17.529,2,12,2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_mark_up.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\">\n    <path\n        android:fillColor=\"@color/main_day_light\"\n        android:pathData=\"M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_music.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FFF\"\n        android:pathData=\"M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_panorama.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"?attr/lv_item_desc_color\"\n        android:pathData=\"M20,6.54v10.91c-2.6,-0.77 -5.28,-1.16 -8,-1.16 -2.72,0 -5.4,0.39 -8,1.16V6.54c2.6,0.77 5.28,1.16 8,1.16 2.72,0.01 5.4,-0.38 8,-1.16M21.43,4c-0.1,0 -0.2,0.02 -0.31,0.06C18.18,5.16 15.09,5.7 12,5.7c-3.09,0 -6.18,-0.55 -9.12,-1.64 -0.11,-0.04 -0.22,-0.06 -0.31,-0.06 -0.34,0 -0.57,0.23 -0.57,0.63v14.75c0,0.39 0.23,0.62 0.57,0.62 0.1,0 0.2,-0.02 0.31,-0.06 2.94,-1.1 6.03,-1.64 9.12,-1.64 3.09,0 6.18,0.55 9.12,1.64 0.11,0.04 0.21,0.06 0.31,0.06 0.33,0 0.57,-0.23 0.57,-0.63V4.63c0,-0.4 -0.24,-0.63 -0.57,-0.63z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_read.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"16dp\"\n    android:height=\"16dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#5a8fca\"\n        android:pathData=\"M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_refresh.xml",
    "content": "<vector android:height=\"24dp\"\n    android:viewportHeight=\"1024.0\"\n    android:viewportWidth=\"1024.0\"\n    android:width=\"24dp\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"?attr/topbar_fg\"\n        android:pathData=\"M887.8,123.2c-21.5,0 -38.9,17.4 -38.9,38.9l0,108.2C771.7,162.4 647.6,97.3 512,97.3c-228.6,0 -414.7,186 -414.7,414.7s186,414.7 414.7,414.7c168.5,0 319.1,-100.7 383.4,-256.5 4.1,-9.9 3.8,-20.6 -0,-29.7 -3.8,-9.2 -11.1,-17 -21.1,-21.1 -9.9,-4.1 -20.5,-3.8 -29.7,0 -9.2,3.8 -16.9,11.2 -21.1,21.1 -52.3,126.6 -174.6,208.4 -311.5,208.4 -185.8,0 -336.9,-151.1 -336.9,-336.9s151.1,-336.9 336.9,-336.9c116.3,0 222.1,58.9 283.8,155.5L680.5,330.6c-21.5,0 -38.9,17.4 -38.9,38.9 0,21.5 17.4,38.9 38.9,38.9l207.3,0c21.5,0 38.9,-17.4 38.9,-38.9L926.7,162.1C926.7,140.7 909.3,123.2 887.8,123.2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_rename.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"?attr/bottombar_fg\"\n        android:pathData=\"M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_share.xml",
    "content": "<vector android:height=\"24dp\"\n    android:viewportHeight=\"1024.0\"\n    android:viewportWidth=\"1024.0\"\n    android:width=\"24dp\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"?attr/topbar_fg\"\n        android:pathData=\"M821.1,276.8c-235.9,25.1 -449.2,226.7 -490.5,452.4a38.4,38.4 0,1 1,-75.6 -13.8c45.6,-248.6 269.3,-468.5 526.7,-510.7l-117.8,-69.2a38.4,38.4 0,0 1,38.9 -66.3l223.3,131.2a38.4,38.4 0,0 1,10.1 57.6l-170.8,206.6a38.4,38.4 0,1 1,-59.2 -48.9l114.8,-138.9z\" />\n    <path\n        android:fillColor=\"?attr/topbar_fg\"\n        android:pathData=\"M832,620.1a38.4,38.4 0,0 1,76.8 0v158.2c0,86 -61.6,157.8 -140.8,157.8H204.8c-79.2,0 -140.8,-71.9 -140.8,-157.9V300.4c0,-86 61.6,-157.8 140.8,-157.8h220.2a38.4,38.4 0,1 1,0 76.8H204.8c-33.9,0 -64,35.1 -64,81V778.2c0,46 30.1,81.1 64,81.1h563.2c33.9,0 64,-35.1 64,-81.1v-158.2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_state_all.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"16dp\"\n    android:height=\"16dp\"\n    android:viewportWidth=\"16.0\"\n    android:viewportHeight=\"16.0\">\n    <path\n        android:fillColor=\"#5a8fca\"\n        android:pathData=\"M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_state_star.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"16dp\"\n    android:height=\"16dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#5a8fca\"\n        android:pathData=\"M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_state_unread.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"16dp\"\n    android:height=\"16dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#5a8fca\"\n        android:pathData=\"M12,2C6.47,2,2,6.47,2,12c0,5.529,4.47,10,10,10c5.529,0,10-4.471,10-10C22,6.47,17.529,2,12,2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_state_unstar.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"16dp\"\n    android:height=\"16dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#5a8fca\"\n        android:pathData=\"M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_stop_loading.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"?attr/topbar_fg\"\n        android:pathData=\"M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8 0,-1.85 0.63,-3.55 1.69,-4.9L16.9,18.31C15.55,19.37 13.85,20 12,20zM18.31,16.9L7.1,5.69C8.45,4.63 10.15,4 12,4c4.42,0 8,3.58 8,8 0,1.85 -0.63,3.55 -1.69,4.9z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_unsubscribe.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"?attr/bottombar_fg\"\n        android:pathData=\"M20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06c-1.13,0.12 -2.19,0.46 -3.16,0.97l1.5,1.5C10.16,5.19 11.06,5 12,5c3.87,0 7,3.13 7,7 0,0.94 -0.19,1.84 -0.52,2.65l1.5,1.5c0.5,-0.96 0.84,-2.02 0.97,-3.15L23,13v-2h-2.06zM3,4.27l2.04,2.04C3.97,7.62 3.25,9.23 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c1.77,-0.2 3.38,-0.91 4.69,-1.98L19.73,21 21,19.73 4.27,3 3,4.27zM16.27,17.54C15.09,18.45 13.61,19 12,19c-3.87,0 -7,-3.13 -7,-7 0,-1.61 0.55,-3.09 1.46,-4.27l9.81,9.81z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_user_agent.xml",
    "content": "<vector android:height=\"24dp\"\n    android:viewportHeight=\"1024.0\"\n    android:viewportWidth=\"1024.0\"\n    android:width=\"24dp\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"?attr/topbar_fg\"\n        android:pathData=\"M876.8,320h-211.2c-70.4,0 -128,57.6 -128,128v358.4c0,70.4 57.6,128 128,128h211.2c70.4,0 128,-57.6 128,-128v-358.4c0,-70.4 -57.6,-128 -128,-128zM665.6,384h211.2c32,0 64,25.6 64,64v320h-332.8v-320c0,-38.4 25.6,-64 57.6,-64zM876.8,864h-211.2c-25.6,0 -44.8,-12.8 -57.6,-38.4h320c-6.4,25.6 -25.6,38.4 -51.2,38.4z\" />\n    <path\n        android:fillColor=\"?attr/topbar_fg\"\n        android:pathData=\"M499.2,704c0,-19.2 -12.8,-32 -32,-32h-326.4c-19.2,0 -38.4,-19.2 -38.4,-38.4v-409.6c0,-19.2 19.2,-32 38.4,-32h627.2c19.2,0 38.4,19.2 38.4,38.4v38.4c0,19.2 12.8,32 32,32s32,-12.8 32,-32v-44.8c0,-51.2 -44.8,-96 -102.4,-96h-627.2c-57.6,0 -102.4,44.8 -102.4,96v409.6c0,57.6 44.8,102.4 102.4,102.4h326.4c25.6,0 32,-19.2 32,-32zM499.2,768h-172.8c-19.2,0 -32,12.8 -32,32s12.8,32 32,32h166.4c19.2,0 32,-12.8 32,-32s-12.8,-32 -25.6,-32z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_volume.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@color/main_day_light\"\n        android:pathData=\"M18.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM5,9v6h4l5,5V4L9,9H5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/logo_feedly.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"200dp\"\n    android:height=\"200dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n  <path\n      android:pathData=\"M965.96,466.77c-117.16,-117.18 -234.35,-234.3 -351.4,-351.53 -17.13,-16.81 -33.22,-36.01 -55.81,-45.65 -22.44,-10.67 -48.43,-12.16 -72.53,-7.04 -21.08,4.84 -40.13,16.41 -55.36,31.62C299.15,225.92 167.39,357.7 35.62,489.39c-15.15,15.15 -27.01,34.05 -31.85,55.08 -10.82,37.87 2.77,80.3 30.51,107.54 92.86,92.93 185.81,185.71 278.63,278.68a110.91,110.91 0,0 0,79.66 33.11c75.97,0.43 151.94,-0.04 227.84,0.26 34.18,2.18 68.76,-10.03 92.95,-34.52 84.14,-83.93 168.06,-168.13 252.16,-252.1 13.72,-13.89 28.57,-26.9 39.72,-43.03 20.5,-29.53 23.89,-69.29 11.93,-102.74 -9.37,-26.84 -31.79,-45.35 -51.2,-64.9z\"\n      android:fillColor=\"#1FB446\"/>\n  <path\n      android:pathData=\"M308.21,616.36c-14.29,17.02 -39.47,12.57 -58.82,9.56 -17.07,-13.67 -32.66,-29.53 -46.42,-46.44 -7.96,-9.37 2.22,-19.16 8.53,-25.75 86.66,-86.27 173.01,-172.93 259.5,-259.35 11.63,-11.31 22.14,-23.89 35.2,-33.6 11.22,-5.61 19.9,6.55 27.39,13.01 15.89,16.94 33.92,32 48.51,50.13 7.53,9 -0.73,19.41 -7.32,25.86 -88.96,88.77 -177.92,177.54 -266.58,266.58zM580.9,820.84c-13.82,15.53 -28.27,30.81 -44.5,43.78 -14.89,3.61 -30.72,2.77 -45.8,0.6 -14.51,-7.34 -24.3,-21.42 -36.16,-32.26 -8.45,-8.23 -21.67,-20.59 -10.77,-32.47a845.42,845.42 0,0 1,58.33 -58.45c7.57,-6.36 18.86,-3.2 24.96,3.73a1921.71,1921.71 0,0 1,51.24 51.33c6.61,5.87 10.18,16.96 2.71,23.74zM575.71,587.95c-49.34,49.3 -98.84,98.52 -147.9,148.12 -13.78,16.87 -37.99,11.52 -56.92,9.96 -18.18,-11.71 -32.32,-29.01 -46.98,-44.78 -9.75,-9.24 -0.64,-21.33 6.89,-28.29 49.41,-49.22 98.75,-98.43 147.9,-147.93 8.96,-8.73 17.07,-18.77 27.93,-25.39 10.86,-4.67 18.82,6.4 26.03,12.59 16.43,17.6 35.16,33.11 50.13,51.97 6.59,8.38 -1.07,17.79 -7.08,23.77z\"\n      android:fillColor=\"#FFFFFF\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/logo_inoreader.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"64\"\n    android:viewportHeight=\"64\">\n\n  <path\n      android:pathData=\"M6.9552,4.5269L56.3976,4.5269A2.7999,2.7999 0,0 1,59.1974 7.3267L59.1974,57.1927A2.7999,2.7999 0,0 1,56.3976 59.9927L6.9552,59.9927A2.7999,2.7999 0,0 1,4.1553 57.1927L4.1553,7.3267A2.7999,2.7999 0,0 1,6.9552 4.5269z\"\n      android:fillColor=\"#38779F\">\n  </path>\n  <path\n      android:pathData=\"m25.4609,10.8965c-0.5385,0.0008 -1.3711,0.0078 -1.3711,0.0078 0.0055,1.5487 0.0052,3.0918 0,4.2012 1.8982,-0.0842 3.8209,0.0481 5.7422,0.4004 12.3059,2.2565 21.0135,13.3923 20.4238,25.8223 0.9858,0 2.9423,-0.0001 4.4746,0.002C55.0973,33.2518 52.0821,25.2601 46.1621,19.373 41.48,14.717 35.8973,11.9706 29.1875,11.0234 28.5779,10.9374 27.2897,10.894 25.4609,10.8965ZM24.498,20.1387c-0.1452,0.0025 -0.2906,0.0061 -0.4355,0.0117 0.0328,1.9227 0.0328,3.2959 0.0156,4.5508 0.2095,-0.0035 0.4442,-0.004 0.7188,-0.0039 1.6133,0.0007 2.5076,0.1065 3.9512,0.4688 4.5287,1.1364 8.3855,4.2771 10.4609,8.5176 1.0404,2.1258 1.5551,4.2207 1.623,6.6152 0.0098,0.3448 0.0093,0.6882 -0.002,1.0293 1.4374,0 3.0548,0 4.5469,-0.0078 0.0111,-1.0628 -0.018,-2.3362 -0.082,-2.875 -0.3949,-3.323 -1.4567,-6.2798 -3.2266,-8.9844 -3.0178,-4.6118 -7.7449,-7.8264 -13.1035,-8.9121 -1.4836,-0.3006 -2.9809,-0.4361 -4.4668,-0.4102z\"\n      android:strokeAlpha=\"1\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"0.00980603\"\n      android:fillColor=\"#ffffff\"\n      android:strokeColor=\"#000000\"\n      android:fillAlpha=\"1\"/>\n  <path\n      android:pathData=\"m23.3784,53.682c-0.5413,-0.0485 -1.0941,-0.1233 -1.5065,-0.204 -4.2984,-0.8405 -7.8489,-3.9741 -9.2428,-8.1574 -0.4365,-1.3101 -0.6221,-2.4699 -0.6239,-3.8986 -0.0013,-1.0569 0.0698,-1.7373 0.282,-2.699 0.9986,-4.5256 4.4857,-8.132 8.9434,-9.2496 0.9212,-0.231 1.6598,-0.3288 2.6696,-0.3535 2.0033,-0.049 3.8498,0.3517 5.6297,1.2219 4.3896,2.1459 7.0969,6.7473 6.8535,11.6484 -0.0709,1.4277 -0.3337,2.659 -0.842,3.9451 -0.7923,2.0046 -2.0624,3.7343 -3.7276,5.0765 -1.7827,1.4369 -3.9054,2.332 -6.194,2.6119 -0.4059,0.0496 -1.9033,0.0886 -2.2413,0.0583zM28.5935,41.2546c1.1186,-0.2949 2.0376,-1.1504 2.4144,-2.2475 0.625,-1.8201 -0.3223,-3.8105 -2.1193,-4.4527 -1.6699,-0.5969 -3.4845,0.1402 -4.2786,1.738 -0.506,1.0181 -0.5066,2.0964 -0.0018,3.1252 0.4839,0.9861 1.3295,1.6438 2.4162,1.8791 0.4166,0.0902 1.1413,0.0708 1.5691,-0.042z\"\n      android:strokeAlpha=\"1\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#ffffff\"\n      android:strokeColor=\"#00000000\"\n      android:fillAlpha=\"1\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/progress_bg.xml",
    "content": "<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!--<item-->\n    <!--android:id=\"@android:id/background\">-->\n    <!--<clip>-->\n    <!--<shape>-->\n    <!--<solid android:color=\"#2e323759\"></solid>-->\n    <!--</shape>-->\n    <!--</clip>-->\n    <!--</item>-->\n    <!--<item-->\n    <!--android:id=\"@android:id/secondaryProgress\"-->\n    <!--&gt;-->\n    <!--<clip>-->\n    <!--<shape>-->\n    <!--<solid android:color=\"#2e32378f\"></solid>-->\n    <!--</shape>-->\n\n    <!--</clip>-->\n    <!--</item>-->\n    <item android:id=\"@android:id/progress\">\n        <clip>\n            <shape>\n                <solid android:color=\"#51a1f1\"></solid>\n            </shape>\n\n        </clip>\n    </item>\n</layer-list>"
  },
  {
    "path": "app/src/main/res/drawable/seekbar_audio.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:id=\"@android:id/background\">\n        <shape>\n            <corners android:radius=\"5dp\" />\n            <solid android:color=\"#c7c7c7\" />\n        </shape>\n    </item>\n    <item android:id=\"@android:id/secondaryProgress\">\n        <clip>\n            <shape>\n                <corners android:radius=\"5dp\" />\n                <solid android:color=\"#9c9c9c\" />\n            </shape>\n        </clip>\n    </item>\n    <item android:id=\"@android:id/progress\">\n        <clip>\n            <shape>\n                <corners android:radius=\"5dp\" />\n                <solid android:color=\"@color/bluePrimary\" />\n            </shape>\n        </clip>\n    </item>\n</layer-list>"
  },
  {
    "path": "app/src/main/res/drawable/selector_corners_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:state_checked=\"false\"\n        android:color=\"#D0D0D0\"\n        android:drawable=\"@drawable/corners_bg_uncheck\" />\n    <item\n        android:state_checked=\"true\"\n        android:color=\"#28A4E9\"\n        android:drawable=\"@drawable/corners_bg_checked\" />\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/selector_star.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:state_checked=\"true\"\n        android:drawable=\"@drawable/ic_favor_fill\"/>\n\n    <!--android:state_focused=\"false\"-->\n    <!--android:state_pressed=\"true\"-->\n    <item\n        android:state_checked=\"false\"\n        android:drawable=\"@drawable/ic_favor\" />\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/splash_layers.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@color/dark_background\" />\n    <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@drawable/logo\" />\n    </item>\n</layer-list>\n"
  },
  {
    "path": "app/src/main/res/drawable/textview_border_day.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\" android:shape=\"rectangle\" >\n    <solid android:color=\"#ececec\" />\n    <corners\n        android:radius=\"5dp\"/>\n    <!--<stroke android:width=\"1dip\" android:color=\"#4fa5d5\"/>-->\n    <!--android:background=\"?attr/root_view_bg\"/>-->\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/textview_border_night.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\" android:shape=\"rectangle\" >\n    <solid android:color=\"@color/black_lv_divider\" />\n    <corners\n        android:radius=\"5dp\"/>\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable-v23/logo_feedly_icon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:drawable=\"@drawable/logo_feedly\"\n        android:width=\"30dp\"\n        android:height=\"30dp\"\n        android:right=\"5dp\">\n    </item>\n</layer-list>\n"
  },
  {
    "path": "app/src/main/res/drawable-v23/logo_inoreader_icon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:drawable=\"@drawable/logo_inoreader\"\n        android:width=\"30dp\"\n        android:height=\"30dp\"\n        android:right=\"5dp\">\n    </item>\n</layer-list>\n"
  },
  {
    "path": "app/src/main/res/drawable-v23/logo_ttrss_icon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:drawable=\"@drawable/logo_tinytinyrss\"\n        android:width=\"30dp\"\n        android:height=\"30dp\"\n        android:right=\"5dp\">\n    </item>\n</layer-list>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_article.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/article_root\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:addStatesFromChildren=\"true\"\n    android:windowDrawsSystemBarBackgrounds=\"true\"\n    tools:background=\"@color/dark_background\"\n    tools:context=\".activity.ArticleActivity\">\n\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/art_appBarLayout\"\n        android:theme=\"@style/ThemeOverlay.AppCompat.Dark.ActionBar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/art_toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/topbar_bg\"\n            app:contentInsetStartWithNavigation=\"0dp\"\n            app:layout_scrollFlags=\"scroll|enterAlways|snap\"\n            app:subtitleTextAppearance=\"@style/MyToolbar.SubTitle\"\n            app:popupTheme=\"@style/OverflowMenuStyle\"\n            tools:navigationIcon=\"@drawable/ic_close\"\n            tools:title=\"源标题\">\n\n            <me.wizos.loread.view.IconFontView\n                android:id=\"@+id/article_feed_config\"\n                android:text=\"@string/font_feed_config\"\n                android:gravity=\"center|end\"\n                android:minWidth=\"40dp\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"match_parent\"\n                android:background=\"?android:attr/selectableItemBackgroundBorderless\"\n                android:layout_gravity=\"end|center\"\n                android:padding=\"5dp\"\n                android:textSize=\"14sp\"\n                android:layout_marginEnd=\"10dp\"\n                android:textColor=\"?attr/topbar_fg\"\n                android:visibility=\"gone\" />\n\n            <!--<TextView-->\n                <!--android:id=\"@+id/art_toolbar_num\"-->\n                <!--android:layout_width=\"wrap_content\"-->\n                <!--android:layout_height=\"match_parent\"-->\n                <!--android:layout_gravity=\"end|center\"-->\n                <!--android:background=\"?android:attr/selectableItemBackgroundBorderless\"-->\n                <!--android:clickable=\"true\"-->\n                <!--android:gravity=\"center|end\"-->\n                <!--android:minWidth=\"40dp\"-->\n                <!--android:textSize=\"12sp\"-->\n                <!--tools:text=\"12\" />-->\n\n        </androidx.appcompat.widget.Toolbar>\n\n        <ProgressBar\n            android:id=\"@+id/article_progress_bar\"\n            style=\"@style/Base.Widget.AppCompat.ProgressBar.Horizontal\"\n            android:layout_width=\"match_parent\"\n            android:progressDrawable=\"@drawable/progress_bg\"\n            android:layout_height=\"2dp\"\n            android:visibility=\"gone\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <me.wizos.loread.view.SwipeRefreshLayoutS\n        android:id=\"@+id/art_swipe_refresh\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n        <me.wizos.loread.view.slideback.SlideLayout\n            android:id=\"@+id/art_slide_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:background=\"?attr/root_view_bg\">\n            <FrameLayout\n                android:id=\"@+id/slide_arrow_layout\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:persistentDrawingCache=\"animation\"\n                android:background=\"?attr/root_view_bg\" />\n        </me.wizos.loread.view.slideback.SlideLayout>\n    </me.wizos.loread.view.SwipeRefreshLayoutS>\n\n    <RelativeLayout\n        android:id=\"@+id/art_bottombar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"@dimen/bottom_bar_height\"\n        android:layout_gravity=\"bottom\"\n        android:layout_alignParentBottom=\"true\"\n        android:background=\"?attr/bottombar_bg\"\n        app:layout_behavior=\"me.wizos.loread.behavior.BottomNavigationViewBehavior\">\n        <View\n            android:id=\"@+id/article_bottombar_divider\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0.5dp\"\n            android:layout_marginStart=\"10dp\"\n            android:layout_marginEnd=\"10dp\"\n            android:layout_alignParentTop=\"true\"\n            android:background=\"?attr/bottombar_divider\" />\n\n        <me.wizos.loread.view.IconFontView\n            android:id=\"@+id/article_bottombar_save\"\n            style=\"@style/bottom_bar_iconfont\"\n            android:layout_alignParentEnd=\"true\"\n            android:text=\"@string/font_unsave\"\n            android:visibility=\"visible\" />\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:paddingStart=\"20dp\"\n            android:paddingEnd=\"20dp\"\n            android:gravity=\"center\"\n            android:orientation=\"horizontal\">\n            <!--<me.wizos.loread.view.IconFontView-->\n            <!--android:id=\"@+id/article_bottombar_tag\"-->\n            <!--style=\"@style/bottom_bar_iconfont\"-->\n            <!--android:text=\"@string/font_label\"-->\n            <!--android:onClick=\"onTagClick\"/>-->\n            <me.wizos.loread.view.IconFontView\n                android:id=\"@+id/article_bottombar_open_link\"\n                style=\"@style/bottom_bar_iconfont\"\n                android:text=\"@string/font_chrome\"\n                android:onClick=\"clickOpenOriginalArticle\" />\n            <me.wizos.loread.view.IconFontView\n                android:id=\"@+id/article_bottombar_readability\"\n                style=\"@style/bottom_bar_iconfont\"\n                android:text=\"@string/font_article_original\"\n                android:onClick=\"onReadabilityClick\" />\n            <me.wizos.loread.view.IconFontView\n                android:id=\"@+id/article_bottombar_star\"\n                style=\"@style/bottom_bar_iconfont\"\n                android:text=\"@string/font_unstar\"\n                android:onClick=\"onClickStarIcon\" />\n\n            <me.wizos.loread.view.IconFontView\n                android:id=\"@+id/article_bottombar_read\"\n                style=\"@style/bottom_bar_iconfont\"\n                android:text=\"@string/font_readed\"\n                android:onClick=\"onReadClick\" />\n        </LinearLayout>\n    </RelativeLayout>\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_feed.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:background=\"?attr/root_view_bg\"\n    tools:context=\".activity.FeedActivity\">\n\n    <!--android:fitsSystemWindows=\"true\"-->\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/feed_app_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"@dimen/app_bar_height\"\n        android:theme=\"@style/AppTheme.Day.AppBarOverlay\">\n\n        <!--app:collapsedTitleTextAppearance=\"@style/MyToolbar.CollapsedToolbarTitle\"-->\n        <com.google.android.material.appbar.CollapsingToolbarLayout\n            android:id=\"@+id/feed_toolbar_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:background=\"?attr/topbar_bg\"\n            app:contentScrim=\"?attr/topbar_bg\"\n            app:statusBarScrim=\"?attr/topbar_bg\"\n            app:layout_scrollFlags=\"scroll|exitUntilCollapsed\"\n            app:expandedTitleTextAppearance=\"@style/MyToolbar.ExpandedToolbarTitle\"\n            app:toolbarId=\"@+id/feed_toolbar\">\n\n            <androidx.appcompat.widget.Toolbar\n                android:id=\"@+id/feed_toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                app:layout_collapseMode=\"pin\"\n                app:layout_scrollFlags=\"scroll|enterAlways|snap\"\n                app:subtitleTextAppearance=\"@style/MyToolbar.SubTitle\"\n                app:popupTheme=\"@style/AppTheme.Day.PopupOverlay\" />\n\n        </com.google.android.material.appbar.CollapsingToolbarLayout>\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <!--<include layout=\"@layout/content_scrolling\" />-->\n    <androidx.core.widget.NestedScrollView\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n        <LinearLayout\n            android:id=\"@+id/feed_summary\"\n            android:padding=\"10dp\"\n            android:orientation=\"vertical\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <LinearLayout\n                android:id=\"@+id/feed_number\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\">\n\n                <TextView\n                    android:visibility=\"gone\"\n                    tools:visibility=\"visible\"\n                    android:id=\"@+id/feed_subscribers\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:lines=\"1\"\n                    android:textSize=\"14sp\"\n                    android:textColor=\"?attr/lv_item_title_color\"\n                    tools:text=\"99\" />\n\n                <TextView\n                    android:visibility=\"gone\"\n                    tools:visibility=\"visible\"\n                    android:id=\"@+id/feed_updated\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginStart=\"20dp\"\n                    android:lines=\"1\"\n                    android:textSize=\"14sp\"\n                    android:textColor=\"?attr/lv_item_title_color\"\n                    tools:text=\"2019/05/01\" />\n            </LinearLayout>\n\n\n            <LinearLayout\n                android:id=\"@+id/feed_desc_layout\"\n                android:layout_marginTop=\"15dp\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\">\n                <TextView\n                    android:id=\"@+id/feed_desc_label\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:lines=\"1\"\n                    android:textSize=\"14sp\"\n                    android:textStyle=\"bold\"\n                    android:textColor=\"?attr/lv_item_title_color\"\n                    android:text=\"@string/feed_desc\" />\n\n                <TextView\n                    android:visibility=\"gone\"\n                    tools:visibility=\"visible\"\n                    android:id=\"@+id/feed_description\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:textSize=\"14sp\"\n                    android:textColor=\"?attr/lv_item_title_color\"\n                    tools:text=\"这里是一大片简介\" />\n            </LinearLayout>\n\n\n            <LinearLayout\n                android:id=\"@+id/feed_site_link_layout\"\n                android:layout_marginTop=\"15dp\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\">\n                <TextView\n                    android:id=\"@+id/feed_site_link_label\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:lines=\"1\"\n                    android:textSize=\"14sp\"\n                    android:textStyle=\"bold\"\n                    android:textColor=\"?attr/lv_item_title_color\"\n                    android:text=\"@string/site_url\" />\n                <TextView\n                    android:id=\"@+id/feed_site_link\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:onClick=\"copyHtmlUrl\"\n                    android:textSize=\"14sp\"\n                    android:textColor=\"?attr/lv_item_title_color\"\n                    tools:text=\"http://blog.wizos.me\" />\n            </LinearLayout>\n\n\n            <LinearLayout\n                android:id=\"@+id/feed_rss_link_layout\"\n                android:layout_marginTop=\"15dp\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\">\n                <TextView\n                    android:id=\"@+id/feed_rss_link_label\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:lines=\"1\"\n                    android:textSize=\"14sp\"\n                    android:textStyle=\"bold\"\n                    android:textColor=\"?attr/lv_item_title_color\"\n                    android:text=\"@string/feed_url\" />\n                <TextView\n                    android:id=\"@+id/feed_rss_link\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:onClick=\"copyFeedUrl\"\n                    android:textSize=\"14sp\"\n                    android:textColor=\"?attr/lv_item_title_color\"\n                    tools:text=\"http://blog.wizos.me/feed\" />\n            </LinearLayout>\n\n            <androidx.appcompat.widget.AppCompatButton\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"10dp\"\n                android:layout_marginBottom=\"10dp\"\n                android:paddingTop=\"10dp\"\n                android:paddingBottom=\"10dp\"\n                android:paddingStart=\"20dp\"\n                android:paddingEnd=\"20dp\"\n                android:layout_gravity=\"center_horizontal\"\n                android:textColor=\"@color/white\"\n                android:onClick=\"clickUnsubscribe\"\n                app:bl_corners_radius=\"30dp\"\n                app:bl_solid_color=\"#ff4343\"\n                app:bl_ripple_enable=\"true\"\n                android:text=\"@string/unsubscribe\"/>\n        </LinearLayout>\n\n    </androidx.core.widget.NestedScrollView>\n\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/fab\"\n        android:onClick=\"copyIconUrl\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"@dimen/fab_margin\"\n        android:padding=\"0dp\"\n        app:layout_anchor=\"@id/feed_app_bar\"\n        app:layout_anchorGravity=\"bottom|end\"\n        app:srcCompat=\"@drawable/logo\" />\n\n\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_lab.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:padding=\"10dp\"\n    android:orientation=\"vertical\"\n    tools:context=\"me.wizos.loread.activity.LabActivity\">\n\n    <!--    <LinearLayout style=\"@style/SettingItem\">-->\n    <!--        <TextView-->\n    <!--            android:id=\"@+id/lab_use_httpdns\"-->\n    <!--            style=\"@style/SettingItemTitle2\"-->\n    <!--            android:text=\"使用HttpDns访问网站\" />-->\n\n    <!--        <com.kyleduo.switchbutton.SwitchButton-->\n    <!--            android:id=\"@+id/lab_use_httpdns_sb\"-->\n    <!--            style=\"@style/SwitchButtonStyle\"-->\n    <!--            android:checked=\"true\" />-->\n    <!--    </LinearLayout>-->\n\n    <TextView\n        android:id=\"@+id/setting_backup\"\n        android:textSize=\"18sp\"\n        android:textColor=\"@color/material_red_400\"\n        style=\"@style/SettingItem\"\n        android:layout_gravity=\"center\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:paddingTop=\"5dp\"\n        android:paddingBottom=\"5dp\"\n        android:gravity=\"center\"\n        android:onClick=\"onClickBackup\"\n        android:text=\"备份未读数据\"\n        android:background=\"?android:attr/selectableItemBackground\"\n        android:clickable=\"true\"\n        android:focusable=\"true\" />\n\n    <TextView\n        android:id=\"@+id/setting_restore\"\n        android:textSize=\"18sp\"\n        android:textColor=\"@color/material_red_400\"\n        style=\"@style/SettingItem\"\n        android:layout_gravity=\"center\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:paddingTop=\"5dp\"\n        android:paddingBottom=\"5dp\"\n        android:gravity=\"center\"\n        android:onClick=\"onClickRestore\"\n        android:text=\"恢复已备份的数据\"\n        android:background=\"?android:attr/selectableItemBackground\"\n        android:clickable=\"true\"\n        android:focusable=\"true\" />\n\n    <TextView\n        android:id=\"@+id/setting_read_config\"\n        android:textSize=\"18sp\"\n        android:textColor=\"@color/material_red_400\"\n        style=\"@style/SettingItem\"\n        android:layout_gravity=\"center\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:paddingTop=\"5dp\"\n        android:paddingBottom=\"5dp\"\n        android:gravity=\"center\"\n        android:onClick=\"onClickReadConfig\"\n        android:text=\"读取配置\"\n        android:background=\"?android:attr/selectableItemBackground\"\n        android:clickable=\"true\"\n        android:focusable=\"true\" />\n\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"50dp\"\n        android:padding=\"10dp\"\n        android:onClick=\"onClickClearHtmlDir\"\n        android:gravity=\"center_vertical\"\n        android:layout_gravity=\"center_vertical\"\n        style=\"@style/SettingItem\"\n        android:text=\"清理因故障未被删除的缓存文件\"/>\n\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"50dp\"\n        android:padding=\"10dp\"\n        android:onClick=\"onClickArrangeCrawlDateArticle\"\n        android:gravity=\"center_vertical\"\n        android:layout_gravity=\"center_vertical\"\n        style=\"@style/SettingItem\"\n        android:text=\"整理文章的爬取时间为发布时间\"/>\n\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"50dp\"\n        android:padding=\"10dp\"\n        android:onClick=\"onClickGenTags\"\n        android:gravity=\"center_vertical\"\n        android:layout_gravity=\"center_vertical\"\n        style=\"@style/SettingItem\"\n        android:text=\"生成 Tag\"/>\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"50dp\"\n        android:padding=\"10dp\"\n        android:onClick=\"onClickClearTags\"\n        android:gravity=\"center_vertical\"\n        android:layout_gravity=\"center_vertical\"\n        style=\"@style/SettingItem\"\n        android:text=\"删除所有 Tag\"/>\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"50dp\"\n        android:padding=\"10dp\"\n        android:onClick=\"startSyncWorkManager\"\n        android:gravity=\"center_vertical\"\n        android:layout_gravity=\"center_vertical\"\n        style=\"@style/SettingItem\"\n        android:text=\"开启 同步任务\"/>\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"50dp\"\n        android:padding=\"10dp\"\n        android:onClick=\"stopWorkManager\"\n        android:gravity=\"center_vertical\"\n        android:layout_gravity=\"center_vertical\"\n        style=\"@style/SettingItem\"\n        android:text=\"关闭 同步任务\"/>\n\n    <EditText\n        android:id=\"@+id/lab_enter_edittext\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_vertical\"\n        android:hint=\"输入\"\n        style=\"@style/SettingItem\"\n        android:padding=\"15dp\"/>\n\n    <TextView\n        android:text=\"打开相应 Activity\"\n        android:textAllCaps=\"false\"\n        android:onClick=\"openActivity\"\n        android:layout_height=\"50dp\"\n        android:padding=\"10dp\"\n        android:gravity=\"center_vertical\"\n        android:layout_gravity=\"center_vertical\"\n        style=\"@style/SettingItem\"/>\n    <TextView\n        android:text=\"修改 Host\"\n        android:onClick=\"onClickEditHost\"\n        android:layout_height=\"50dp\"\n        android:padding=\"10dp\"\n        android:gravity=\"center_vertical\"\n        android:layout_gravity=\"center_vertical\"\n        style=\"@style/SettingItem\"/>\n\n    <ScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n        <LinearLayout\n            android:orientation=\"vertical\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n\n            <TextView\n                android:text=\"根据action处理文章\"\n                android:textAllCaps=\"false\"\n                android:onClick=\"actionArticle\"\n                android:layout_height=\"50dp\"\n                android:padding=\"10dp\"\n                android:gravity=\"center_vertical\"\n                android:layout_gravity=\"center_vertical\"\n                style=\"@style/SettingItem\"/>\n        </LinearLayout>\n    </ScrollView>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_login_inoreader.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".activity.login.LoginInoReaderActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/inoreader_appBarLayout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"@style/ThemeOverlay.AppCompat.Dark.ActionBar\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/inoreader_toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/topbar_bg\"\n            app:contentInsetStartWithNavigation=\"0dp\"\n            app:layout_scrollFlags=\"scroll|enterAlways|snap\"\n            app:subtitleTextAppearance=\"@style/MyToolbar.SubTitle\"\n            app:navigationIcon=\"@drawable/ic_close\"\n            app:title=\"@string/login\">\n        </androidx.appcompat.widget.Toolbar>\n    </com.google.android.material.appbar.AppBarLayout>\n    \n        <me.wizos.loread.view.FriendlyCardView\n            android:id=\"@+id/inoreader_login_form\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/inoreader_appBarLayout\"\n            android:layout_marginStart=\"30dp\"\n            android:layout_marginEnd=\"30dp\"\n            app:cardMaxElevation=\"10dp\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:padding=\"10dp\"\n                android:orientation=\"vertical\">\n\n                <me.zhanghai.android.materialedittext.MaterialTextInputLayout\n                    android:id=\"@+id/inoreader_host_layout\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    app:errorEnabled=\"true\">\n                    <me.zhanghai.android.materialedittext.MaterialEditText\n                        android:id=\"@+id/inoreader_host_edittext\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:hint=\"@string/server_host\"\n                        android:text=\"@string/inoreader_url\"\n                        android:inputType=\"textUri\"\n                        android:maxLines=\"1\" />\n                </me.zhanghai.android.materialedittext.MaterialTextInputLayout>\n\n                <me.zhanghai.android.materialedittext.MaterialTextInputLayout\n                    android:id=\"@+id/inoreader_username_layout\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    app:errorEnabled=\"true\">\n\n                    <me.zhanghai.android.materialedittext.MaterialEditText\n                        android:id=\"@+id/inoreader_username_edittext\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:hint=\"@string/account\"\n                        android:inputType=\"textEmailAddress\"\n                        android:maxLines=\"1\" />\n                </me.zhanghai.android.materialedittext.MaterialTextInputLayout>\n\n                <me.zhanghai.android.materialedittext.MaterialTextInputLayout\n                    android:id=\"@+id/inoreader_password_layout\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    app:errorEnabled=\"true\">\n\n                    <me.zhanghai.android.materialedittext.MaterialEditText\n                        android:id=\"@+id/inoreader_password_edittext\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:hint=\"@string/auth_password\"\n                        android:imeActionId=\"@+id/ime_login\"\n                        android:imeActionLabel=\"@string/login\"\n                        android:imeOptions=\"actionDone\"\n                        android:inputType=\"textPassword\"\n                        android:maxLines=\"1\"\n                        tools:ignore=\"InvalidImeActionId\" />\n                </me.zhanghai.android.materialedittext.MaterialTextInputLayout>\n\n                <Button\n                    android:id=\"@+id/inoreader_login_button\"\n                    style=\"@style/Widget.AppCompat.Button.Colored\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"end\"\n                    android:enabled=\"false\"\n                    android:text=\"@string/login\" />\n            </LinearLayout>\n\n        </me.wizos.loread.view.FriendlyCardView>\n\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:alpha=\"0.8\"\n        android:ellipsize=\"end\"\n        android:gravity=\"center\"\n        android:maxLines=\"1\"\n        android:text=\"@string/the_server_host_like\"\n        android:textColor=\"@color/dark_background\"\n        android:textSize=\"12dp\"\n        android:visibility=\"gone\"\n        app:layout_constraintEnd_toEndOf=\"@+id/inoreader_login_form\"\n        app:layout_constraintHorizontal_bias=\"0.0\"\n        app:layout_constraintStart_toStartOf=\"@+id/inoreader_login_form\"\n        app:layout_constraintTop_toBottomOf=\"@+id/inoreader_login_form\"\n        tools:ignore=\"MissingConstraints,SpUsage\" />\n\n    <ProgressBar\n        android:id=\"@+id/inoreader_loading\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_marginStart=\"32dp\"\n        android:layout_marginTop=\"64dp\"\n        android:layout_marginEnd=\"32dp\"\n        android:layout_marginBottom=\"64dp\"\n        android:visibility=\"gone\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"@+id/inoreader_login_form\"\n        app:layout_constraintStart_toStartOf=\"@+id/inoreader_login_form\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_login_tiny_rss.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/tiny_rss_container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".activity.login.LoginTinyRSSActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/tiny_rss_appBarLayout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"@style/ThemeOverlay.AppCompat.Dark.ActionBar\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/tiny_rss_toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/topbar_bg\"\n            app:contentInsetStartWithNavigation=\"0dp\"\n            app:layout_scrollFlags=\"scroll|enterAlways|snap\"\n            app:subtitleTextAppearance=\"@style/MyToolbar.SubTitle\"\n            app:navigationIcon=\"@drawable/ic_close\"\n            app:title=\"@string/login\">\n        </androidx.appcompat.widget.Toolbar>\n    </com.google.android.material.appbar.AppBarLayout>\n\n\n    <!--当ScrollView里的元素想填满ScrollView时，使用\"fill_parent\"是不管用的，必需为ScrollView设置：android:fillViewport=\"true\"。-->\n    <!--当ScrollView没有fillVeewport=“true”时, 里面的元素(比如LinearLayout)会按照wrap_content来计算(不论它是否设了\"fill_parent\")-->\n    <!--而如果LinearLayout的元素设置了fill_parent,那么也是不管用的，因为LinearLayout依赖里面的元素，而里面的元素又依赖LinearLayout,这样自相矛盾-->\n    <!--所以里面元素设置了fill_parent，也会当做wrap_content来计算.-->\n\n<!--    <LinearLayout-->\n<!--        android:id=\"@+id/login_form\"-->\n<!--        android:layout_width=\"match_parent\"-->\n<!--        android:layout_height=\"wrap_content\"-->\n<!--        android:gravity=\"center_horizontal\"-->\n<!--        android:orientation=\"vertical\"-->\n<!--        app:layout_constraintBottom_toBottomOf=\"parent\"-->\n<!--        app:layout_constraintTop_toBottomOf=\"@+id/tiny_rss_appBarLayout\"-->\n<!--        tools:layout_editor_absoluteX=\"0dp\">-->\n\n<!--        <Space-->\n<!--            android:layout_width=\"0dp\"-->\n<!--            android:layout_height=\"0dp\"-->\n<!--            android:layout_weight=\"0\" />-->\n\n        <!-- Using app:cardMaxElevation as margin. -->\n\n        <me.wizos.loread.view.FriendlyCardView\n            android:id=\"@+id/tiny_rss_login_form\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/tiny_rss_appBarLayout\"\n            android:layout_marginStart=\"30dp\"\n            android:layout_marginEnd=\"30dp\"\n            app:cardMaxElevation=\"10dp\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:padding=\"10dp\"\n                android:orientation=\"vertical\">\n\n                <me.zhanghai.android.materialedittext.MaterialTextInputLayout\n                    android:id=\"@+id/tiny_rss_host_layout\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    app:errorEnabled=\"true\">\n                    <me.zhanghai.android.materialedittext.MaterialEditText\n                        android:id=\"@+id/tiny_rss_host_edittext\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:hint=\"@string/server_host\"\n                        android:text=\"@string/https_scheme\"\n                        android:inputType=\"textUri\"\n                        android:maxLines=\"1\" />\n                </me.zhanghai.android.materialedittext.MaterialTextInputLayout>\n\n                <me.zhanghai.android.materialedittext.MaterialTextInputLayout\n                    android:id=\"@+id/tiny_rss_username_layout\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    app:errorEnabled=\"true\">\n\n                    <me.zhanghai.android.materialedittext.MaterialEditText\n                        android:id=\"@+id/tiny_rss_username_edittext\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:hint=\"@string/account\"\n                        android:inputType=\"textEmailAddress\"\n                        android:maxLines=\"1\" />\n                </me.zhanghai.android.materialedittext.MaterialTextInputLayout>\n\n                <me.zhanghai.android.materialedittext.MaterialTextInputLayout\n                    android:id=\"@+id/tiny_rss_password_layout\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    app:errorEnabled=\"true\">\n\n                    <me.zhanghai.android.materialedittext.MaterialEditText\n                        android:id=\"@+id/tiny_rss_password_edittext\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:hint=\"@string/auth_password\"\n                        android:imeActionId=\"@+id/ime_login\"\n                        android:imeActionLabel=\"@string/login\"\n                        android:imeOptions=\"actionDone\"\n                        android:inputType=\"textPassword\"\n                        android:maxLines=\"1\"\n                        tools:ignore=\"InvalidImeActionId\" />\n                </me.zhanghai.android.materialedittext.MaterialTextInputLayout>\n\n                <Button\n                    android:id=\"@+id/tiny_rss_login_button\"\n                    style=\"@style/Widget.AppCompat.Button.Colored\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"end\"\n                    android:enabled=\"false\"\n                    android:text=\"@string/login\" />\n            </LinearLayout>\n\n<!--            <Space-->\n<!--                android:id=\"@+id/login_space_a\"-->\n<!--                android:layout_width=\"0dp\"-->\n<!--                android:layout_height=\"0dp\"-->\n<!--                android:layout_weight=\"1\" />-->\n\n            <!--<me.zhanghai.android.materialprogressbar.MaterialProgressBar-->\n            <!--android:id=\"@+id/progress\"-->\n            <!--android:layout_width=\"wrap_content\"-->\n            <!--android:layout_height=\"wrap_content\"-->\n            <!--android:layout_gravity=\"center\"-->\n            <!--android:indeterminate=\"true\"-->\n            <!--android:visibility=\"invisible\"-->\n            <!--style=\"@style/Widget.MaterialProgressBar.ProgressBar\" />-->\n\n        </me.wizos.loread.view.FriendlyCardView>\n\n\n    <!--        <Space-->\n<!--            android:layout_width=\"0dp\"-->\n<!--            android:layout_height=\"0dp\"-->\n<!--            android:layout_weight=\"3\" />-->\n<!--    </LinearLayout>-->\n\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:alpha=\"0.8\"\n        android:ellipsize=\"end\"\n        android:gravity=\"center\"\n        android:maxLines=\"1\"\n        android:text=\"@string/the_server_host_like\"\n        android:textColor=\"@color/dark_background\"\n        android:textSize=\"12dp\"\n        android:visibility=\"gone\"\n        app:layout_constraintEnd_toEndOf=\"@+id/tiny_rss_login_form\"\n        app:layout_constraintHorizontal_bias=\"0.0\"\n        app:layout_constraintStart_toStartOf=\"@+id/tiny_rss_login_form\"\n        app:layout_constraintTop_toBottomOf=\"@+id/tiny_rss_login_form\"\n        tools:ignore=\"MissingConstraints,SpUsage\" />\n\n    <ProgressBar\n        android:id=\"@+id/tiny_rss_loading\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_marginStart=\"32dp\"\n        android:layout_marginTop=\"64dp\"\n        android:layout_marginEnd=\"32dp\"\n        android:layout_marginBottom=\"64dp\"\n        android:visibility=\"gone\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"@+id/tiny_rss_login_form\"\n        app:layout_constraintStart_toStartOf=\"@+id/tiny_rss_login_form\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/main_root\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:addStatesFromChildren=\"true\"\n    android:windowDrawsSystemBarBackgrounds=\"true\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/main_appBarLayout\"\n        android:theme=\"@style/ThemeOverlay.AppCompat.Dark.ActionBar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/main_toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/topbar_bg\"\n            tools:title=\"博谈\"\n            tools:navigationIcon=\"@drawable/ic_state_star\"\n            app:contentInsetStartWithNavigation=\"0dp\"\n            app:subtitleTextAppearance=\"@style/MyToolbar.SubTitle\"\n            app:layout_scrollFlags=\"scroll|enterAlways\">\n\n            <androidx.appcompat.widget.AppCompatImageView\n                android:id=\"@+id/main_toolbar_auto_mark\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:visibility=\"gone\"\n                tools:visibility=\"visible\"\n                android:src=\"@drawable/ic_arrow_auto_mark_readed\" />\n        </androidx.appcompat.widget.Toolbar>\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <me.wizos.loread.view.SwipeRefreshLayoutS\n        android:id=\"@+id/main_swipe_refresh\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_below=\"@+id/main_appBarLayout\"\n        android:background=\"?attr/root_view_bg\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n        tools:context=\".activity.MainActivity\">\n        <RelativeLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n\n            <me.wizos.loread.view.IconFontView\n                android:id=\"@+id/main_placeholder\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_centerInParent=\"true\"\n                android:layout_marginBottom=\"@dimen/bottom_bar_height\"\n                android:text=\"@string/font_no_data\"\n                android:textSize=\"50sp\" />\n\n            <com.yanzhenjie.recyclerview.SwipeRecyclerView\n                android:id=\"@+id/main_slv\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:layout_above=\"@+id/main_bottombar\"\n                android:choiceMode=\"singleChoice\"\n                android:headerDividersEnabled=\"false\"\n                app:layout_behavior=\"@string/appbar_scrolling_view_behavior\" />\n\n            <RelativeLayout\n                android:id=\"@+id/main_bottombar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"@dimen/bottom_bar_height\"\n                android:layout_gravity=\"center_vertical|bottom\"\n                android:layout_alignParentBottom=\"true\"\n                tools:background=\"@color/white_toolbar_bg\">\n\n                <View\n                    android:id=\"@+id/main_bottombar_divider\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"0.5dp\"\n                    android:layout_marginStart=\"10dp\"\n                    android:layout_marginEnd=\"10dp\"\n                    android:background=\"?attr/bottombar_divider\" />\n\n                <me.wizos.loread.view.IconFontView\n                    android:id=\"@+id/main_bottombar_tag\"\n                    style=\"@style/bottom_bar_iconfont\"\n                    android:layout_centerInParent=\"true\"\n                    android:text=\"@string/font_group\"\n                    android:onClick=\"onClickCategoryIcon\" />\n\n                <me.wizos.loread.view.IconFontView\n                    android:id=\"@+id/main_bottombar_search\"\n                    style=\"@style/bottom_bar_iconfont\"\n                    android:layout_toStartOf=\"@id/main_bottombar_tag\"\n                    android:text=\"@string/font_search\"\n                    android:onClick=\"clickSearchIcon\" />\n\n                <me.wizos.loread.view.IconFontView\n                    android:id=\"@+id/main_bottombar_setting\"\n                    style=\"@style/bottom_bar_iconfont\"\n                    android:text=\"@string/font_more\"\n                    android:layout_toEndOf=\"@id/main_bottombar_tag\"\n                    android:onClick=\"onQuickSettingIconClicked\" />\n\n                <me.wizos.loread.view.IconFontView\n                    android:id=\"@+id/main_bottombar_refresh_articles\"\n                    style=\"@style/bottom_bar_iconfont\"\n                    android:layout_alignParentEnd=\"true\"\n                    android:visibility=\"gone\"\n                    android:text=\"@string/font_update\"\n                    android:onClick=\"clickRefreshIcon\" />\n            </RelativeLayout>\n        </RelativeLayout>\n    </me.wizos.loread.view.SwipeRefreshLayoutS>\n</RelativeLayout>\n\n"
  },
  {
    "path": "app/src/main/res/layout/activity_main_list_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/main_slv_item\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center_vertical\"\n    android:layout_marginTop=\"6dp\"\n    android:layout_marginBottom=\"6dp\"\n    tools:ShowIn=\"@layout/activity_main\"\n    android:orientation=\"vertical\"\n    android:baselineAligned=\"false\">\n\n    <!--android:background=\"?attr/root_view_bg\"-->\n\n    <RelativeLayout\n        android:id=\"@+id/main_list_item_surface\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:paddingStart=\"12dp\"\n        android:paddingEnd=\"12dp\"\n        android:background=\"?attr/root_view_bg\"\n        tools:ignore=\"UselessParent\">\n\n        <RelativeLayout\n            android:id=\"@+id/main_slv_item_text\"\n\n            android:layout_marginTop=\"6dp\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <LinearLayout\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:id=\"@+id/main_slv_item_title_state\"\n                android:layout_alignParentStart=\"true\"\n                android:layout_toStartOf=\"@+id/main_slv_item_img\"\n                android:orientation=\"horizontal\">\n\n                <me.wizos.loread.view.IconFontView\n                    android:id=\"@+id/main_slv_item_icon_star\"\n                    android:layout_width=\"16dp\"\n                    android:layout_height=\"16dp\"\n                    android:visibility=\"gone\"\n                    tools:visibility=\"visible\"\n                    android:textColor=\"#5a8fca\"\n                    android:gravity=\"center\"\n                    android:textSize=\"12sp\"\n                    android:text=\"@string/font_stared\" />\n\n                <me.wizos.loread.view.IconFontView\n                    android:id=\"@+id/main_slv_item_icon_reading\"\n                    android:layout_width=\"16dp\"\n                    android:layout_height=\"16dp\"\n                    android:visibility=\"gone\"\n                    tools:visibility=\"visible\"\n                    android:textColor=\"#5a8fca\"\n                    android:gravity=\"center\"\n                    android:textSize=\"12sp\"\n                    android:text=\"@string/font_unread\" />\n\n                <me.wizos.loread.view.IconFontView\n                    android:id=\"@+id/main_slv_item_icon_save\"\n                    android:layout_width=\"16dp\"\n                    android:layout_height=\"16dp\"\n                    android:visibility=\"gone\"\n                    tools:visibility=\"visible\"\n                    android:textColor=\"#5a8fca\"\n                    android:gravity=\"center\"\n                    android:textSize=\"12sp\"\n                    android:text=\"@string/font_saved\" />\n                <!--android:maxLines=\"1\"-->\n                <TextView\n                    android:id=\"@+id/main_slv_item_title\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:gravity=\"center_vertical\"\n                    android:fontFamily=\"sans-serif\"\n                    android:textStyle=\"bold\"\n                    android:includeFontPadding=\"false\"\n                    android:ellipsize=\"end\"\n                    android:maxLines=\"2\"\n                    android:textSize=\"15sp\"\n                    tools:text=\"这里就是一个大标题这里就是一个大标题这里就是一个大标题\"\n                    android:textColor=\"?attr/lv_item_title_color\" />\n            </LinearLayout>\n\n            <TextView\n                android:id=\"@+id/main_slv_item_summary\"\n                android:layout_marginTop=\"4dp\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_below=\"@id/main_slv_item_title_state\"\n                android:layout_toStartOf=\"@id/main_slv_item_img\"\n                android:layout_alignParentStart=\"true\"\n                android:fontFamily=\"sans-serif\"\n                android:maxLines=\"2\"\n                android:ellipsize=\"end\"\n                android:textSize=\"13sp\"\n                android:layout_marginEnd=\"2dp\"\n                tools:text=\"简介简介简介简介简介简介简介简介简介简介简介简介简介简介\"\n                android:textColor=\"?attr/lv_item_desc_color\" />\n\n            <ImageView\n                android:id=\"@id/main_slv_item_img\"\n                android:layout_width=\"80dp\"\n                android:layout_height=\"60dp\"\n                android:adjustViewBounds=\"true\"\n                android:layout_marginStart=\"6dp\"\n                android:maxWidth=\"96dp\"\n                android:maxHeight=\"72dp\"\n                android:layout_alignParentEnd=\"true\"\n                android:layout_alignParentTop=\"true\"\n                android:background = \"@color/placeholder_bg\"\n                android:visibility=\"gone\"\n                tools:visibility=\"visible\"\n                tools:ignore=\"ContentDescription\" />\n\n        </RelativeLayout>\n\n        <RelativeLayout\n            android:id=\"@+id/main_slv_item_info\"\n            android:layout_marginTop=\"6dp\"\n            android:layout_marginBottom=\"6dp\"\n            android:layout_below=\"@id/main_slv_item_text\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <TextView\n                android:id=\"@+id/main_slv_item_author\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginEnd=\"6dp\"\n                android:layout_toStartOf=\"@+id/main_slv_item_time\"\n                android:layout_alignParentStart=\"true\"\n                android:textSize=\"12sp\"\n                android:ellipsize=\"end\"\n                android:maxLines=\"1\"\n                tools:text=\"文章作者fdsfsd是电饭锅电饭锅\"\n                android:textColor=\"?attr/lv_item_info_color\" />\n\n            <TextView\n                android:id=\"@+id/main_slv_item_time\"\n                android:layout_alignParentEnd=\"true\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginEnd=\"6dp\"\n                android:layout_marginStart=\"2dp\"\n                android:gravity=\"end\"\n                android:maxLines=\"1\"\n                android:textSize=\"12sp\"\n                tools:text=\"文章时间\"\n                android:textColor=\"?attr/lv_item_info_color\" />\n        </RelativeLayout>\n\n        <View\n            android:id=\"@+id/main_slv_item_divider\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0.5dp\"\n            android:layout_below=\"@id/main_slv_item_info\"\n            android:background=\"?attr/lv_item_divider\" />\n\n    </RelativeLayout>\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_music.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\"me.wizos.loread.activity.MusicActivity\">\n\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/music_appbar_layout\"\n        android:theme=\"@style/ThemeOverlay.AppCompat.Dark.ActionBar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/music_toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"?attr/actionBarSize\"\n        android:background=\"?attr/topbar_bg\"\n        tools:title=\"博谈\"\n        tools:navigationIcon=\"@drawable/ic_music\"\n        app:contentInsetStartWithNavigation=\"0dp\"\n        app:subtitleTextAppearance=\"@style/MyToolbar.SubTitle\"\n        app:layout_scrollFlags=\"scroll|enterAlways\">\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/main_toolbar_auto_mark\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:visibility=\"gone\"\n            tools:visibility=\"visible\"\n            android:src=\"@drawable/ic_arrow_auto_mark_readed\" />\n    </androidx.appcompat.widget.Toolbar>\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <LinearLayout\n        android:id=\"@+id/music_toggle_container\"\n        android:layout_above=\"@id/music_seekbar_container\"\n        android:orientation=\"horizontal\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"@dimen/bottom_bar_height\"\n        android:gravity=\"center_vertical\"\n        android:background=\"?attr/root_view_bg\"\n        android:paddingStart=\"17dp\"\n        android:paddingEnd=\"17dp\"\n        android:paddingTop=\"4dp\"\n        android:paddingBottom=\"4dp\">\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/music_close\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"30dp\"\n            android:src=\"@drawable/ic_close\"/>\n        <TextView\n            android:id=\"@+id/music_title\"\n            android:layout_weight=\"1\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"10dp\"\n            android:layout_marginEnd=\"10dp\"\n            android:gravity=\"center_vertical\"\n            android:lines=\"1\"\n            android:textSize=\"12sp\"\n            android:textColor=\"?attr/lv_item_title_color\"\n            tools:text=\"天地骨任我行\" />\n\n        <TextView\n            android:id=\"@+id/music_speed\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"30dp\"\n            android:gravity=\"center\"\n            android:layout_marginStart=\"10dp\"\n            android:layout_marginEnd=\"20dp\"\n            android:text=\"@string/music_speed\"\n            android:textColor=\"?attr/lv_item_desc_color\"/>\n\n        <com.freedom.lauzy.playpauseviewlib.PlayPauseView\n            android:id=\"@+id/music_play_pause_view\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"30dp\"\n            app:anim_direction=\"positive\"\n            app:anim_duration=\"300\"\n            app:bg_color=\"#E0E0E0\"\n            app:btn_color=\"#282828\"\n            app:gap_width=\"3dp\"/>\n    </LinearLayout>\n\n    <RelativeLayout\n        android:id=\"@+id/music_seekbar_container\"\n        android:layout_alignParentBottom=\"true\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"@dimen/bottom_bar_height\"\n        android:background=\"?attr/root_view_bg\"\n        android:layout_marginBottom=\"10dp\"\n        android:padding=\"7dp\">\n\n        <TextView\n            android:id=\"@+id/currTime\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentStart=\"true\"\n            android:layout_centerVertical=\"true\"\n            android:layout_marginStart=\"10dp\"\n            android:text=\"@string/music_time\"\n            android:textColor=\"?attr/lv_item_desc_color\"/>\n\n        <SeekBar\n            android:id=\"@+id/progressBar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_centerVertical=\"true\"\n            android:layout_marginLeft=\"3dp\"\n            android:layout_marginRight=\"3dp\"\n            android:layout_toEndOf=\"@+id/currTime\"\n            android:layout_toStartOf=\"@+id/totalTime\"\n            android:padding=\"10dp\"\n            android:progressDrawable=\"@drawable/seekbar_audio\"\n            android:thumb=\"@drawable/custom_progress_bar_thumb\" />\n\n        <TextView\n            android:id=\"@+id/totalTime\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentEnd=\"true\"\n            android:layout_centerVertical=\"true\"\n            android:layout_marginEnd=\"10dp\"\n            android:text=\"@string/music_time\"\n            android:textColor=\"?attr/lv_item_desc_color\" />\n    </RelativeLayout>\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_provider.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/provider_root\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:fitsSystemWindows=\"true\"\n    android:addStatesFromChildren=\"true\"\n    android:windowDrawsSystemBarBackgrounds=\"true\"\n    tools:context=\".activity.ProviderActivity\">\n\n    <TextView\n        android:id=\"@+id/provider_rss_title\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"80dp\"\n        android:layout_marginBottom=\"20dp\"\n        android:gravity=\"center\"\n        android:text=\"@string/everything_is_rssible\"\n        android:textSize=\"20sp\"\n        android:textStyle=\"bold\" />\n\n    <TextView\n        android:id=\"@+id/provider_rss_summary\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"20dp\"\n        android:textSize=\"16sp\"\n        android:text=\"@string/rss_summary\" />\n\n    <Space\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:layout_weight=\"1\"/>\n\n    <LinearLayout\n        android:layout_margin=\"10dp\"\n        android:layout_width=\"220dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:gravity=\"center\"\n        android:orientation=\"vertical\">\n\n        <Button\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:drawableStart=\"@drawable/logo_inoreader_icon\"\n            android:minWidth=\"150dp\"\n            android:onClick=\"oauthInoReader\"\n            android:textAllCaps=\"false\"\n            android:text=\"@string/inoreader_oauth\" />\n\n        <Button\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:drawableStart=\"@drawable/logo_inoreader_icon\"\n            android:minWidth=\"150dp\"\n            android:onClick=\"loginInoReader\"\n            android:textAllCaps=\"false\"\n            android:text=\"@string/inoreader_login\" />\n\n        <Button\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:drawableStart=\"@drawable/logo_feedly_icon\"\n            android:minWidth=\"150dp\"\n            android:onClick=\"oauthFeedly\"\n            android:textAllCaps=\"false\"\n            android:text=\"@string/feedly\" />\n\n        <Button\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:drawableStart=\"@drawable/logo_ttrss_icon\"\n            android:minWidth=\"150dp\"\n            android:onClick=\"loginTinyRSS\"\n            android:textAllCaps=\"false\"\n            android:text=\"@string/tinytinyrss\" />\n    </LinearLayout>\n\n<!--    android:textColor=\"?attr/lv_item_title_color\"-->\n    <TextView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_marginTop=\"20dp\"\n        android:layout_marginBottom=\"20dp\"\n        android:alpha=\"0.6\"\n        android:text=\"@string/developed_by_wizos\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_provider_low_version.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/provider_root\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:fitsSystemWindows=\"true\"\n    android:addStatesFromChildren=\"true\"\n    android:windowDrawsSystemBarBackgrounds=\"true\"\n    tools:context=\".activity.ProviderActivity\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:orientation=\"vertical\"\n        android:layout_height=\"0dp\"\n        android:layout_weight=\"1\">\n\n        <TextView\n            android:id=\"@+id/provider_rss_title\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_margin=\"30dp\"\n            android:gravity=\"center\"\n            android:text=\"@string/everything_is_rssible\"\n            android:textStyle=\"bold\"\n            android:textSize=\"20sp\" />\n\n        <TextView\n            android:id=\"@+id/provider_rss_summary\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"242dp\"\n            android:layout_margin=\"20dp\"\n            android:textSize=\"16sp\"\n            android:text=\"@string/rss_summary\" />\n    </LinearLayout>\n\n    <LinearLayout\n        android:layout_width=\"230dp\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"10dp\"\n        android:layout_gravity=\"center\"\n        android:orientation=\"vertical\">\n        <LinearLayout\n            android:onClick=\"oauthInoReader\"\n            android:layout_margin=\"10dp\"\n            android:orientation=\"horizontal\"\n            android:gravity=\"center\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n            <ImageView\n                android:id=\"@+id/provider_inoreader_logo\"\n                android:src=\"@drawable/logo_inoreader\"\n                android:layout_marginEnd=\"5dp\"\n                android:layout_width=\"40dp\"\n                android:layout_height=\"40dp\" />\n            <Button\n                android:id=\"@+id/provider_inoreader_title\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:clickable=\"false\"\n                android:textAllCaps=\"false\"\n                android:text=\"@string/inoreader_oauth\"/>\n        </LinearLayout>\n\n        <LinearLayout\n            android:onClick=\"oauthInoReader\"\n            android:layout_margin=\"10dp\"\n            android:orientation=\"horizontal\"\n            android:gravity=\"center\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n            <ImageView\n                android:id=\"@+id/provider_inoreader_login_logo\"\n                android:src=\"@drawable/logo_inoreader\"\n                android:layout_marginEnd=\"5dp\"\n                android:layout_width=\"40dp\"\n                android:layout_height=\"40dp\" />\n            <Button\n                android:id=\"@+id/provider_inoreader_login_title\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:clickable=\"false\"\n                android:textAllCaps=\"false\"\n                android:text=\"@string/inoreader_login\"/>\n        </LinearLayout>\n\n        <LinearLayout\n            android:id=\"@+id/provider_feedly\"\n            android:onClick=\"oauthFeedly\"\n            android:layout_margin=\"10dp\"\n            android:orientation=\"horizontal\"\n            android:gravity=\"center\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n            <ImageView\n                android:id=\"@+id/provider_feedly_logo\"\n                android:src=\"@drawable/logo_feedly\"\n                android:layout_marginEnd=\"5dp\"\n                android:layout_width=\"40dp\"\n                android:layout_height=\"40dp\" />\n            <Button\n                android:id=\"@+id/provider_feedly_title\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:clickable=\"false\"\n                android:textAllCaps=\"false\"\n                android:text=\"@string/feedly\"/>\n        </LinearLayout>\n\n        <LinearLayout\n            android:onClick=\"loginTinyRSS\"\n            android:layout_margin=\"10dp\"\n            android:orientation=\"horizontal\"\n            android:gravity=\"center\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n            <ImageView\n                android:id=\"@+id/provider_ttrss_logo\"\n                android:src=\"@drawable/logo_tinytinyrss\"\n                android:layout_marginEnd=\"5dp\"\n                android:layout_width=\"40dp\"\n                android:layout_height=\"40dp\" />\n            <Button\n                android:id=\"@+id/provider_ttrss_title\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:clickable=\"false\"\n                android:textAllCaps=\"false\"\n                android:text=\"@string/tinytinyrss\"/>\n        </LinearLayout>\n\n<!--        <RelativeLayout-->\n<!--            android:layout_margin=\"10dp\"-->\n<!--            android:layout_width=\"match_parent\"-->\n<!--            android:layout_height=\"wrap_content\">-->\n<!--            <ImageView-->\n<!--                android:layout_alignParentStart=\"true\"-->\n<!--                android:id=\"@+id/provider_localrss_logo\"-->\n<!--                android:layout_width=\"40dp\"-->\n<!--                android:layout_height=\"40dp\" />-->\n<!--            <TextView-->\n<!--                android:layout_toEndOf=\"@id/provider_localrss_logo\"-->\n<!--                android:id=\"@+id/provider_localrss_title\"-->\n<!--                android:layout_width=\"match_parent\"-->\n<!--                android:layout_height=\"wrap_content\"-->\n<!--                android:maxLines=\"2\"-->\n<!--                android:ellipsize=\"end\"-->\n<!--                android:fontFamily=\"sans-serif\"-->\n<!--                android:textSize=\"15sp\"-->\n<!--                android:text=\"本地RSS\"/>-->\n<!--            <TextView-->\n<!--                android:layout_below=\"@id/provider_localrss_title\"-->\n<!--                android:id=\"@+id/provider_local-rss_summary\"-->\n<!--                android:layout_toEndOf=\"@id/provider_localrss_logo\"-->\n<!--                android:layout_marginTop=\"4dp\"-->\n<!--                android:layout_width=\"wrap_content\"-->\n<!--                android:layout_height=\"wrap_content\"-->\n<!--                android:maxLines=\"2\"-->\n<!--                android:ellipsize=\"end\"-->\n<!--                android:fontFamily=\"sans-serif\"-->\n<!--                android:textSize=\"13sp\"-->\n<!--                android:text=\"在本地搭建RSS服务器，耗费流量较多\"/>-->\n<!--        </RelativeLayout>-->\n\n    </LinearLayout>\n\n    <TextView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_marginTop=\"20dp\"\n        android:layout_marginBottom=\"20dp\"\n        android:alpha=\"0.6\"\n        android:text=\"@string/developed_by_wizos\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_rule_generate.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/rule_generate_root\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:addStatesFromChildren=\"true\"\n    android:windowDrawsSystemBarBackgrounds=\"true\"\n    tools:context=\"me.wizos.loread.activity.RuleGenerateActivity\">\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/rule_generate_appBarLayout\"\n        android:theme=\"@style/ThemeOverlay.AppCompat.Dark.ActionBar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/rule_generate_toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/topbar_bg\"\n            app:navigationIcon=\"@drawable/ic_close\"\n            app:contentInsetStartWithNavigation=\"0dp\"\n            app:subtitleTextAppearance=\"@style/MyToolbar.SubTitle\"\n            app:layout_scrollFlags=\"scroll|enterAlways\"\n            app:popupTheme=\"@style/OverflowMenuStyle\"\n            tools:title=\"网页标题\" />\n        <ProgressBar\n            android:id=\"@+id/rule_generate_progress_bar\"\n            style=\"@style/Base.Widget.AppCompat.ProgressBar.Horizontal\"\n            android:layout_width=\"match_parent\"\n            android:progressDrawable=\"@drawable/progress_bg\"\n            android:layout_height=\"2dp\"\n            android:visibility=\"gone\" />\n    </com.google.android.material.appbar.AppBarLayout>\n    <ScrollView\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n        android:id=\"@+id/rule_generate_webview_frame\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        tools:layout_height=\"222dp\"/>\n\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n\n"
  },
  {
    "path": "app/src/main/res/layout/activity_search.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/search_root\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:addStatesFromChildren=\"true\"\n    android:focusable=\"true\"\n    android:focusableInTouchMode=\"true\"\n    android:windowDrawsSystemBarBackgrounds=\"true\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/search_appBarLayout\"\n        android:theme=\"@style/ThemeOverlay.AppCompat.Dark.ActionBar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/search_toolbar\"\n            app:layout_scrollFlags=\"scroll|enterAlways\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/topbar_bg\"\n            app:contentInsetStartWithNavigation=\"0dp\"\n            app:subtitleTextAppearance=\"@style/MyToolbar.SubTitle\">\n            <!--<me.wizos.loread.view.IconFontView-->\n            <!--android:id=\"@+id/search_toolbar_clear_icon\"-->\n            <!--style=\"@style/top_bar_iconfont\"-->\n            <!--android:layout_gravity=\"end|center\"-->\n            <!--android:layout_weight=\"1\"-->\n            <!--android:visibility=\"gone\"-->\n            <!--android:text=\"@string/font_clear_search_word\"/>-->\n            <com.xw.repo.XEditText\n                android:id=\"@+id/search_toolbar_edittext\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginEnd=\"15dp\"\n                android:hint=\"@string/search\"\n                android:imeOptions=\"actionSearch\"\n                android:singleLine=\"true\" />\n\n        </androidx.appcompat.widget.Toolbar>\n\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <me.wizos.loread.view.SwipeRefreshLayoutS\n        android:id=\"@+id/search_swipe_refresh\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"?attr/root_view_bg\"\n        android:layout_below=\"@+id/search_appBarLayout\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n        tools:context=\".activity.SearchActivity\">\n\n        <ListView\n            android:id=\"@+id/search_list_view\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:divider=\"?attr/lv_item_divider\"\n            android:layout_below=\"@id/search_appBarLayout\"\n            android:groupIndicator=\"@null\"\n            android:fadingEdgeLength=\"4dp\"\n            android:background=\"?attr/bottombar_bg\" />\n    </me.wizos.loread.view.SwipeRefreshLayoutS>\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_search_list_header_result_count.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/search_header_word\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center_vertical\"\n    android:background=\"?attr/bottombar_bg\"\n    tools:ShowIn=\"@layout/activity_search\">\n\n    <TextView\n        android:id=\"@+id/search_feeds_result_count\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"5dp\"\n        android:layout_marginBottom=\"5dp\"\n        android:layout_gravity=\"center\"\n        android:padding=\"10dp\"\n        android:gravity=\"center\"\n        android:text=\"@string/search_cloudy_feeds_result_count\"\n        android:textColor=\"?attr/lv_item_desc_color\" />\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_search_list_header_word.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/search_header_word\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center_vertical\"\n    android:background=\"?attr/bottombar_bg\"\n    tools:ShowIn=\"@layout/activity_search\">\n\n    <TextView\n        android:id=\"@+id/search_intent_feeds\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"5dp\"\n        android:layout_marginBottom=\"10dp\"\n        android:layout_gravity=\"center\"\n        android:padding=\"10dp\"\n        android:gravity=\"center\"\n        android:text=\"@string/search_header_search_cloudy_feeds\"\n        android:background=\"?android:attr/selectableItemBackground\"\n        android:clickable=\"true\"\n        android:textColor=\"?attr/lv_item_desc_color\"\n        android:onClick=\"onSearchFeedsClicked\" />\n\n    <TextView\n        android:id=\"@+id/search_local_articles\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"10dp\"\n        android:layout_marginBottom=\"5dp\"\n        android:layout_gravity=\"center\"\n        android:padding=\"10dp\"\n        android:gravity=\"center\"\n        android:text=\"@string/search_header_search_local_articles\"\n        android:background=\"?android:attr/selectableItemBackground\"\n        android:clickable=\"true\"\n        android:textColor=\"?attr/lv_item_desc_color\"\n        android:onClick=\"onSearchLocalArtsClicked\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_search_list_item_feed.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/search_list_item\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center_vertical\"\n    android:paddingTop=\"6dp\"\n    android:paddingBottom=\"6dp\"\n    android:paddingStart=\"10dp\"\n    android:paddingEnd=\"10dp\"\n    android:background=\"?attr/root_view_bg\"\n    tools:ShowIn=\"@layout/activity_search\"\n    android:orientation=\"vertical\"\n    android:baselineAligned=\"false\">\n    <!--MarginStart指的是控件距离开头View部分的间距大小，MarginLeft则指的是控件距离左边View部分的间距大小，MarginEnd和MarginRight同理。-->\n    <!--一般情况下，View开始部分就是左边，但是有的语言目前为止还是按照从右往左的顺序来书写的-->\n\n    <LinearLayout\n        android:id=\"@+id/search_list_item_text\"\n        android:layout_marginTop=\"6dp\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center_vertical\"\n        android:orientation=\"horizontal\">\n\n        <ImageView\n            android:id=\"@+id/search_list_item_icon\"\n            android:layout_width=\"22dp\"\n            android:layout_height=\"16dp\"\n            android:paddingEnd=\"6dp\"\n            android:layout_gravity=\"center_vertical\"\n            android:adjustViewBounds=\"true\"\n            tools:visibility=\"visible\"\n            tools:ignore=\"ContentDescription\" />\n\n        <LinearLayout\n            android:id=\"@+id/search_list_item_title_summary\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\"\n            android:orientation=\"vertical\">\n\n            <TextView\n                android:id=\"@+id/search_list_item_title\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:gravity=\"center_vertical\"\n                android:fontFamily=\"sans-serif\"\n                android:textStyle=\"bold\"\n                android:includeFontPadding=\"false\"\n                android:maxLines=\"1\"\n                android:ellipsize=\"end\"\n                android:textSize=\"14sp\"\n                tools:text=\"这里就是一个大标题这里就是一个大标题这里就是一个大标题\"\n                android:textColor=\"?attr/lv_item_title_color\" />\n\n            <TextView\n                android:id=\"@+id/search_list_item_summary\"\n                android:layout_marginTop=\"2dp\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:fontFamily=\"sans-serif\"\n                android:maxLines=\"4\"\n                android:ellipsize=\"end\"\n                android:textSize=\"13sp\"\n                android:layout_marginEnd=\"0dp\"\n                tools:text=\"简介简介简介简介简介简介简介简介简介简介简介简介简介简介\"\n                android:textColor=\"?attr/lv_item_desc_color\" />\n\n            <TextView\n                android:id=\"@+id/search_list_item_feed_url\"\n                android:layout_marginTop=\"2dp\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:fontFamily=\"sans-serif\"\n                android:maxLines=\"1\"\n                android:ellipsize=\"end\"\n                android:textSize=\"13sp\"\n                android:layout_marginEnd=\"0dp\"\n                tools:text=\"http://loread.xyz/feed\"\n                android:textColor=\"?attr/lv_item_desc_color\" />\n        </LinearLayout>\n\n        <me.wizos.loread.view.IconFontView\n            android:id=\"@+id/search_list_item_sub_state\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"30dp\"\n            android:layout_gravity=\"center_vertical\"\n            android:gravity=\"center\"\n            android:textSize=\"13sp\"\n            android:textColor=\"?attr/lv_item_title_color\" />\n    </LinearLayout>\n\n    <RelativeLayout\n        android:id=\"@+id/search_list_item_info\"\n        android:layout_marginTop=\"6dp\"\n        android:layout_marginBottom=\"6dp\"\n        android:layout_below=\"@id/search_list_item_text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <TextView\n            android:id=\"@+id/search_list_item_sub_velocity\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginEnd=\"6dp\"\n            android:layout_marginStart=\"2dp\"\n            android:textSize=\"12sp\"\n            tools:text=\"订阅人数\"\n            android:textColor=\"?attr/lv_item_info_color\" />\n\n        <TextView\n            android:id=\"@+id/search_list_item_last_updated\"\n            android:layout_alignParentEnd=\"true\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginEnd=\"6dp\"\n            android:layout_marginStart=\"2dp\"\n            android:gravity=\"end\"\n            android:textSize=\"12sp\"\n            tools:text=\"上次更新时间\"\n            android:textColor=\"?attr/lv_item_info_color\" />\n    </RelativeLayout>\n\n    <View\n        android:id=\"@+id/search_list_item_divider\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0.5dp\"\n        android:layout_below=\"@id/search_list_item_info\"\n        android:background=\"?attr/lv_item_divider\" />\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_setting.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:addStatesFromChildren=\"true\"\n    android:id=\"@+id/setting_coordinator\"\n    tools:context=\"me.wizos.loread.activity.SettingActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/appBarLayout\"\n        android:theme=\"@style/AppTheme.AppBarOverlay\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/setting_toolbar\"\n            android:layout_width=\"match_parent\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\"\n            android:background=\"?attr/topbar_bg\"\n            android:layout_height=\"?attr/actionBarSize\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.core.widget.NestedScrollView\n        android:id=\"@+id/setting_scroll\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"?attr/root_view_bg\"\n\n        tools:background=\"#e9e9e9\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:paddingBottom=\"@dimen/activity_vertical_margin\"\n            android:orientation=\"vertical\">\n\n            <TextView\n                android:id=\"@+id/setting_sync_icon\"\n                style=\"@style/SettingItemHeader\"\n                android:text=\"@string/sync\" />\n            <LinearLayout style=\"@style/SettingItem\">\n\n                <TextView\n                    android:id=\"@+id/setting_auto_sync_title\"\n                    style=\"@style/SettingItemTitle2\"\n                    android:text=\"@string/setting_auto_sync_title\" />\n                <com.kyleduo.switchbutton.SwitchButton\n                    android:id=\"@+id/setting_auto_sync_sb\"\n                    style=\"@style/SwitchButtonStyle\"\n                    android:onClick=\"onSBClick\"\n                    android:checked=\"true\" />\n            </LinearLayout>\n\n            <LinearLayout\n                android:id=\"@+id/setting_auto_sync_on_wifi\"\n                tools:visibility=\"visible\"\n                android:visibility=\"gone\"\n                style=\"@style/SettingItem\">\n\n                <TextView\n                    android:id=\"@+id/setting_auto_sync_on_wifi_title\"\n                    android:layout_width=\"0dp\"\n                    android:layout_weight=\"1\"\n                    style=\"@style/SettingItemTitle2\"\n                    android:text=\"@string/setting_auto_sync_on_wifi_title\" />\n                <com.kyleduo.switchbutton.SwitchButton\n                    android:id=\"@+id/setting_auto_sync_on_wifi_sb\"\n                    style=\"@style/SwitchButtonStyle\"\n                    android:onClick=\"onSBClick\"\n                    android:checked=\"true\" />\n            </LinearLayout>\n\n            <LinearLayout\n                android:id=\"@+id/setting_auto_sync_frequency\"\n                tools:visibility=\"visible\"\n                android:visibility=\"gone\"\n                android:onClick=\"onClickAutoSyncFrequencySelect\"\n                style=\"@style/SettingItem\">\n                <TextView\n                    style=\"@style/SettingItemTitleHorizontal\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:text=\"@string/setting_sync_frequency_title\" />\n                <TextView\n                    android:id=\"@+id/setting_sync_frequency_summary\"\n                    style=\"@style/SettingItemSummary\"\n                    tools:text=\"30 分钟\" />\n            </LinearLayout>\n\n            <LinearLayout\n                android:id=\"@+id/setting_down_img\"\n                style=\"@style/SettingItem\"\n                android:gravity=\"center_vertical\">\n\n                <TextView\n                    android:id=\"@+id/setting_down_img_title\"\n                    style=\"@style/SettingItemTitle2\"\n                    android:text=\"@string/setting_down_img_title\" />\n                <com.kyleduo.switchbutton.SwitchButton\n                    android:id=\"@+id/setting_down_img_sb\"\n                    style=\"@style/SwitchButtonStyle\"\n                    android:allowUndo=\"false\"\n                    android:checked=\"false\"\n                    android:onClick=\"onSBClick\" />\n            </LinearLayout>\n\n            <TextView\n                android:id=\"@+id/setting_display_icon\"\n                style=\"@style/SettingItemHeader\"\n                tools:text=\"界面样式\"\n                android:text=\"@string/other\" />\n\n\n            <LinearLayout\n                android:id=\"@+id/setting_auto_toggle_theme\"\n                style=\"@style/SettingItem\"\n                android:gravity=\"center_vertical\">\n\n                <TextView\n                    android:id=\"@+id/setting_auto_toggle_theme_title\"\n                    android:layout_width=\"0dp\"\n                    android:layout_weight=\"1\"\n                    style=\"@style/SettingItemTitle2\"\n                    android:text=\"@string/setting_auto_toggle_theme_title\" />\n                <com.kyleduo.switchbutton.SwitchButton\n                    android:id=\"@+id/setting_auto_toggle_theme_sb\"\n                    style=\"@style/SwitchButtonStyle\"\n                    android:onClick=\"onSBClick\"\n                    android:checked=\"true\" />\n            </LinearLayout>\n\n            <LinearLayout\n                android:id=\"@+id/setting_link_open_mode\"\n                style=\"@style/SettingItem\">\n\n                <TextView\n                    android:id=\"@+id/setting_link_open_mode_title\"\n                    style=\"@style/SettingItemTitle2\"\n                    android:text=\"@string/open_link_by_system_browser\" />\n                <com.kyleduo.switchbutton.SwitchButton\n                    android:id=\"@+id/setting_link_open_mode_sb\"\n                    style=\"@style/SwitchButtonStyle\"\n                    android:checked=\"false\"\n                    android:onClick=\"onSBClick\" />\n            </LinearLayout>\n\n            <LinearLayout\n                android:id=\"@+id/setting_clear_day\"\n                android:onClick=\"showClearBeforeDay\"\n                style=\"@style/SettingItem\">\n                <TextView\n                    android:id=\"@+id/setting_clear_day_title\"\n                    style=\"@style/SettingItemTitle2\"\n                    android:text=\"@string/setting_clear_day_title\" />\n                <TextView\n                    android:id=\"@+id/setting_clear_day_summary\"\n                    style=\"@style/SettingItemSummary\"\n                    tools:text=\"10 天\" />\n            </LinearLayout>\n\n            <TextView\n                style=\"@style/SettingItemHeader\"\n                android:text=\"@string/about\" />\n\n\n            <LinearLayout\n                android:visibility=\"gone\"\n                android:id=\"@+id/setting_license\"\n                style=\"@style/SettingItem\"\n                android:orientation=\"horizontal\">\n                <TextView\n                    android:id=\"@+id/setting_license_title\"\n                    style=\"@style/SettingItemTitleHorizontal\"\n                    android:text=\"@string/license\" />\n                <TextView\n                    android:id=\"@+id/setting_license_summary\"\n                    style=\"@style/SettingItemSummary\"\n                    android:text=\"More\" />\n            </LinearLayout>\n\n            <LinearLayout\n                android:id=\"@+id/setting_join_qqgroup\"\n                style=\"@style/SettingItem\"\n                android:onClick=\"joinQQGroup\"\n                android:orientation=\"horizontal\">\n                <TextView\n                    android:id=\"@+id/setting_join_qqgroup_title\"\n                    style=\"@style/SettingItemTitleHorizontal\"\n                    android:text=\"@string/qq_group\" />\n                <TextView\n                    android:id=\"@+id/setting_join_qqgroup_summary\"\n                    style=\"@style/SettingItemSummary\"\n                    android:text=\"@string/qq_group_number\" />\n            </LinearLayout>\n            <LinearLayout\n                android:id=\"@+id/setting_feedback\"\n                style=\"@style/SettingItem\"\n                android:onClick=\"onClickFeedback\"\n                android:orientation=\"horizontal\">\n\n                <TextView\n                    android:id=\"@+id/setting_feedback_title\"\n                    style=\"@style/SettingItemTitleHorizontal\"\n                    android:text=\"@string/feedback\" />\n\n                <TextView\n                    android:id=\"@+id/setting_feedback_summary\"\n                    style=\"@style/SettingItemSummary\" />\n            </LinearLayout>\n\n            <LinearLayout\n                android:id=\"@+id/setting_about\"\n                style=\"@style/SettingItem\"\n                android:onClick=\"showAbout\"\n                android:orientation=\"horizontal\">\n\n                <TextView\n                    android:id=\"@+id/setting_about_title\"\n                    style=\"@style/SettingItemTitleHorizontal\"\n                    android:text=\"@string/about\" />\n\n                <TextView\n                    android:id=\"@+id/setting_about_summary\"\n                    style=\"@style/SettingItemSummary\" />\n            </LinearLayout>\n\n            <TextView\n                tools:visibility=\"visible\"\n                android:visibility=\"gone\"\n                android:id=\"@+id/setting_lab\"\n                android:textSize=\"18sp\"\n                android:textColor=\"@color/material_red_400\"\n                style=\"@style/SettingItem\"\n                android:layout_gravity=\"center\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:paddingTop=\"5dp\"\n                android:paddingBottom=\"5dp\"\n                android:gravity=\"center\"\n                android:text=\"实验室\"\n                android:background=\"?android:attr/selectableItemBackground\"\n                android:clickable=\"true\"\n                android:focusable=\"true\" />\n\n            <TextView\n                android:id=\"@+id/setting_switch_user\"\n                android:textSize=\"18sp\"\n                android:textColor=\"@color/material_red_400\"\n                style=\"@style/SettingItem\"\n                android:layout_gravity=\"center\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:paddingTop=\"5dp\"\n                android:paddingBottom=\"5dp\"\n                android:gravity=\"center\"\n                android:onClick=\"onClickSwitchUser\"\n                android:text=\"@string/switch_account\"\n                android:background=\"?android:attr/selectableItemBackground\" />\n        </LinearLayout>\n    </androidx.core.widget.NestedScrollView>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_tts.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\"me.wizos.loread.activity.TTSActivity\">\n\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/music_appbar_layout\"\n        android:theme=\"@style/ThemeOverlay.AppCompat.Dark.ActionBar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/music_toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"?attr/actionBarSize\"\n        android:background=\"?attr/topbar_bg\"\n        tools:title=\"博谈\"\n        tools:navigationIcon=\"@drawable/ic_music\"\n        app:contentInsetStartWithNavigation=\"0dp\"\n        app:subtitleTextAppearance=\"@style/MyToolbar.SubTitle\"\n        app:layout_scrollFlags=\"scroll|enterAlways\">\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/main_toolbar_auto_mark\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:visibility=\"gone\"\n            tools:visibility=\"visible\"\n            android:src=\"@drawable/ic_arrow_auto_mark_readed\" />\n    </androidx.appcompat.widget.Toolbar>\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <LinearLayout\n        android:id=\"@+id/music_toggle_container\"\n        android:layout_above=\"@id/music_seekbar_container\"\n        android:orientation=\"horizontal\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"@dimen/bottom_bar_height\"\n        android:gravity=\"center_vertical\"\n        android:background=\"?attr/root_view_bg\"\n        android:paddingStart=\"17dp\"\n        android:paddingEnd=\"17dp\"\n        android:paddingTop=\"4dp\"\n        android:paddingBottom=\"4dp\">\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/music_close\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"30dp\"\n            android:src=\"@drawable/ic_close\"/>\n        <TextView\n            android:id=\"@+id/music_title\"\n            android:layout_weight=\"1\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"10dp\"\n            android:layout_marginEnd=\"10dp\"\n            android:gravity=\"center_vertical\"\n            android:lines=\"1\"\n            android:textSize=\"12sp\"\n            android:textColor=\"?attr/lv_item_title_color\"\n            tools:text=\"天地骨任我行\" />\n\n        <TextView\n            android:id=\"@+id/music_speed\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"30dp\"\n            android:gravity=\"center\"\n            android:layout_marginStart=\"10dp\"\n            android:layout_marginEnd=\"20dp\"\n            android:text=\"@string/music_speed\"\n            android:textColor=\"?attr/lv_item_desc_color\"/>\n\n        <com.freedom.lauzy.playpauseviewlib.PlayPauseView\n            android:id=\"@+id/music_play_pause_view\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"30dp\"\n            app:anim_direction=\"positive\"\n            app:anim_duration=\"300\"\n            app:bg_color=\"#E0E0E0\"\n            app:btn_color=\"#282828\"\n            app:gap_width=\"3dp\"/>\n    </LinearLayout>\n\n    <RelativeLayout\n        android:id=\"@+id/music_seekbar_container\"\n        android:layout_alignParentBottom=\"true\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"@dimen/bottom_bar_height\"\n        android:background=\"?attr/root_view_bg\"\n        android:layout_marginBottom=\"10dp\"\n        android:padding=\"7dp\">\n\n        <TextView\n            android:id=\"@+id/currTime\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentStart=\"true\"\n            android:layout_centerVertical=\"true\"\n            android:layout_marginStart=\"10dp\"\n            android:text=\"@string/music_time\"\n            android:textColor=\"?attr/lv_item_desc_color\"/>\n\n        <SeekBar\n            android:id=\"@+id/progressBar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_centerVertical=\"true\"\n            android:layout_marginLeft=\"3dp\"\n            android:layout_marginRight=\"3dp\"\n            android:layout_toEndOf=\"@+id/currTime\"\n            android:layout_toStartOf=\"@+id/totalTime\"\n            android:padding=\"10dp\"\n            android:progressDrawable=\"@drawable/seekbar_audio\"\n            android:thumb=\"@drawable/custom_progress_bar_thumb\" />\n\n        <TextView\n            android:id=\"@+id/totalTime\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentEnd=\"true\"\n            android:layout_centerVertical=\"true\"\n            android:layout_marginEnd=\"10dp\"\n            android:text=\"@string/music_time\"\n            android:textColor=\"?attr/lv_item_desc_color\" />\n    </RelativeLayout>\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_web.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/web_root\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:addStatesFromChildren=\"true\"\n    android:windowDrawsSystemBarBackgrounds=\"true\"\n    tools:context=\"me.wizos.loread.activity.WebActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/web_appBarLayout\"\n        android:theme=\"@style/ThemeOverlay.AppCompat.Dark.ActionBar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/web_toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/topbar_bg\"\n            app:navigationIcon=\"@drawable/ic_close\"\n            app:contentInsetStartWithNavigation=\"0dp\"\n            app:subtitleTextAppearance=\"@style/MyToolbar.SubTitle\"\n            app:popupTheme=\"@style/OverflowMenuStyle\"\n            app:layout_scrollFlags=\"scroll|enterAlways|snap\"\n            tools:title=\"网页标题\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/bottom_sheet_category.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/sheet_tag\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?attr/root_view_bg\">\n\n<!--    android:layout_below=\"@id/header_item\"-->\n    <com.yanzhenjie.recyclerview.SwipeRecyclerView\n        android:id=\"@+id/main_tag_list_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"/>\n    <!--<com.yanzhenjie.recyclerview.StickyHeaderLayout-->\n        <!--android:id=\"@+id/sticky_header_layout\"-->\n        <!--android:layout_below=\"@id/header_item\"-->\n        <!--android:layout_width=\"match_parent\"-->\n        <!--android:layout_height=\"wrap_content\">-->\n    <!--</com.yanzhenjie.recyclerview.StickyHeaderLayout>-->\n\n\n    <me.wizos.loread.view.IconFontView\n        tools:visibility=\"visible\"\n        android:id=\"@+id/main_tag_close\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"40dp\"\n        android:padding=\"5dp\"\n        android:visibility=\"gone\"\n        android:gravity=\"center\"\n        android:textSize=\"20sp\"\n        android:textColor=\"?attr/bottombar_fg\"\n        android:layout_centerHorizontal=\"true\"\n        android:layout_alignParentBottom=\"true\"\n        android:text=\"@string/font_cross\" />\n\n</RelativeLayout>\n\n"
  },
  {
    "path": "app/src/main/res/layout/config_download_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:focusable=\"true\"\n    android:focusableInTouchMode=\"true\">\n\n    <EditText\n        android:id=\"@+id/file_name_edit\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"40dp\"\n        android:gravity=\"center_vertical\"\n        android:textSize=\"14sp\"\n        tools:text=\"13sdddddddddddd答复打发斯蒂芬毒贩夫妇p\"\n        android:ellipsize=\"end\"\n        android:singleLine=\"true\" />\n\n    <TextView\n        android:id=\"@+id/file_download_dir\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"30dp\"\n        android:gravity=\"center_vertical\"\n        android:textSize=\"12sp\"\n        android:text=\"@string/download_file_position\"\n        android:layout_below=\"@id/file_name_edit\" />\n\n    <TextView\n        android:id=\"@+id/file_size\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"30dp\"\n        android:gravity=\"center_vertical\"\n        android:textSize=\"12sp\"\n        tools:text=\"文件大小\"\n        android:layout_below=\"@id/file_download_dir\" />\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/main_bottom_sheet_more.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?attr/root_view_bg\"\n    android:orientation=\"vertical\"\n    android:paddingStart=\"20dp\"\n    android:paddingEnd=\"10dp\"\n    android:paddingTop=\"10dp\">\n\n    <LinearLayout\n        android:id=\"@+id/more_setting\"\n        style=\"@style/SettingItem\">\n\n        <TextView\n            android:id=\"@+id/more_setting_title\"\n            style=\"@style/BottomSheetItemTitle\"\n            android:text=\"@string/more_setting\" />\n\n        <me.wizos.loread.view.IconFontView\n            android:id=\"@+id/more_setting_icon\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"match_parent\"\n            android:gravity=\"center_vertical|end\"\n            android:paddingEnd=\"10dp\"\n            android:textColor=\"?attr/lv_item_title_color\"\n            android:text=\"@string/font_arrow_right\" />\n    </LinearLayout>\n\n    <LinearLayout style=\"@style/SettingItem\">\n\n        <TextView\n            android:id=\"@+id/auto_mark_when_scrolling_title\"\n            style=\"@style/BottomSheetItemTitle\"\n            android:text=\"@string/auto_mark_when_scrolling\" />\n\n        <com.kyleduo.switchbutton.SwitchButton\n            android:id=\"@+id/auto_mark_when_scrolling_switch\"\n            style=\"@style/SwitchButtonStyle\" />\n    </LinearLayout>\n\n    <LinearLayout style=\"@style/SettingItem\">\n\n        <TextView\n            android:id=\"@+id/down_img_on_wifi_title\"\n            style=\"@style/BottomSheetItemTitle\"\n            android:text=\"@string/down_img_on_wifi\" />\n\n        <com.kyleduo.switchbutton.SwitchButton\n            android:id=\"@+id/down_img_on_wifi_switch\"\n            style=\"@style/SwitchButtonStyle\" />\n    </LinearLayout>\n\n\n    <LinearLayout style=\"@style/SettingItem\">\n\n        <TextView\n            android:id=\"@+id/night_theme_title\"\n            style=\"@style/BottomSheetItemTitle\"\n            android:text=\"@string/night_theme\" />\n\n        <com.kyleduo.switchbutton.SwitchButton\n            android:id=\"@+id/night_theme_switch\"\n            style=\"@style/SwitchButtonStyle\" />\n    </LinearLayout>\n\n<!--    <LinearLayout style=\"@style/SettingItem\">-->\n<!--        <TextView-->\n<!--            android:id=\"@+id/include_valueless_title\"-->\n<!--            style=\"@style/BottomSheetItemTitle\"-->\n<!--            android:text=\"@string/include_valueless\" />-->\n\n<!--        <com.kyleduo.switchbutton.SwitchButton-->\n<!--            android:id=\"@+id/include_valueless_switch\"-->\n<!--            style=\"@style/SwitchButtonStyle\" />-->\n<!--    </LinearLayout>-->\n\n    <LinearLayout style=\"@style/SettingItem\">\n\n        <TextView\n            android:id=\"@+id/article_list_state_title\"\n            style=\"@style/BottomSheetItemTitle\"\n            android:text=\"@string/article_list_state\" />\n\n        <RadioGroup\n            android:id=\"@+id/article_list_state_radio_group\"\n            android:paddingLeft=\"10dp\"\n            android:paddingRight=\"10dp\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"match_parent\"\n            android:orientation=\"horizontal\"\n            android:gravity=\"center_vertical\">\n\n            <RadioButton\n                android:checked=\"false\"\n                android:id=\"@+id/radio_all\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:paddingLeft=\"10dp\"\n                android:paddingRight=\"10dp\"\n                android:paddingTop=\"4dp\"\n                android:paddingBottom=\"4dp\"\n                android:textSize=\"12sp\"\n                android:text=\"@string/all\"\n                android:button=\"@null\"\n                android:textColor=\"@drawable/selector_corners_bg\"\n                android:background=\"@drawable/corners_bg_uncheck\" />\n\n            <RadioButton\n                android:id=\"@+id/radio_unread\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:paddingLeft=\"10dp\"\n                android:paddingRight=\"10dp\"\n                android:paddingTop=\"4dp\"\n                android:paddingBottom=\"4dp\"\n                android:layout_marginStart=\"10dp\"\n                android:checked=\"true\"\n                android:text=\"@string/unread\"\n                android:textSize=\"12sp\"\n                android:button=\"@null\"\n                android:textColor=\"@drawable/selector_corners_bg\"\n                android:background=\"@drawable/corners_bg_uncheck\" />\n\n            <RadioButton\n                android:checked=\"false\"\n                android:id=\"@+id/radio_starred\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:paddingLeft=\"10dp\"\n                android:paddingRight=\"10dp\"\n                android:paddingTop=\"4dp\"\n                android:paddingBottom=\"4dp\"\n                android:layout_marginStart=\"10dp\"\n                android:text=\"@string/starred\"\n                android:textSize=\"12sp\"\n                android:button=\"@null\"\n                android:textColor=\"@drawable/selector_corners_bg\"\n                android:background=\"@drawable/corners_bg_uncheck\" />\n        </RadioGroup>\n    </LinearLayout>\n\n    <me.wizos.loread.view.IconFontView\n        android:id=\"@+id/main_more_close\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"40dp\"\n        android:padding=\"5dp\"\n        android:gravity=\"center\"\n        android:textSize=\"20sp\"\n        android:textColor=\"?attr/bottombar_fg\"\n        android:background=\"?attr/root_view_bg\"\n        android:text=\"@string/font_cross\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/main_item_header.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/main_header\"\n    android:orientation=\"horizontal\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center_vertical\"\n    android:background=\"?attr/root_view_bg\"\n    android:paddingStart=\"12dp\"\n    android:paddingEnd=\"12dp\"\n    tools:ShowIn=\"@layout/activity_main\">\n\n    <TextView\n        android:id=\"@+id/main_header_title\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:paddingTop=\"10dp\"\n        android:paddingBottom=\"10dp\"\n        android:layout_weight=\"1\"\n        android:gravity=\"center_vertical\"\n        android:lines=\"1\"\n        android:textSize=\"12sp\"\n        android:textStyle=\"bold\"\n        android:textColor=\"?attr/lv_item_desc_color\"\n        tools:text=\"收藏220\" />\n    <androidx.appcompat.widget.AppCompatImageView\n        android:id=\"@+id/main_header_eye\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:paddingTop=\"7dp\"\n        android:paddingBottom=\"7dp\"\n        android:paddingStart=\"10dp\"\n        android:paddingEnd=\"10dp\"\n        android:src=\"@drawable/ic_eye\"/>\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/md_simplelist_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"start|center_vertical\"\n    android:minHeight=\"62dp\"\n    android:orientation=\"horizontal\"\n    android:paddingEnd=\"@dimen/md_dialog_frame_margin\"\n    android:paddingStart=\"@dimen/md_dialog_frame_margin\">\n\n    <ImageView\n        android:id=\"@android:id/icon\"\n        android:layout_width=\"28dp\"\n        android:layout_height=\"28dp\"\n        android:layout_gravity=\"start|center_vertical\"\n        android:layout_marginEnd=\"16dp\"\n        android:background=\"@color/white\"\n        android:scaleType=\"fitXY\"\n        tools:background=\"#f5f5f5\"\n        tools:src=\"@drawable/logo_inoreader\"\n        tools:ignore=\"ContentDescription\" />\n<!--    tools:src=\"@drawable/flyme_style_switch_button_round\"-->\n\n    <TextView\n        android:id=\"@android:id/title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_vertical\"\n        android:textSize=\"18sp\"\n        tools:text=\"Title\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/setting_item_arrow.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/setting_item\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"60dp\"\n    android:gravity=\"center_vertical\"\n    android:background=\"?android:selectableItemBackground\"\n    android:padding=\"10dp\"\n    android:baselineAligned=\"false\">\n\n    <!--android:layout_marginTop=\"6dp\"-->\n    <!--android:layout_marginBottom=\"6dp\"-->\n    <!--<LinearLayout-->\n        <!--android:id=\"@+id/setting_item_title_desc\"-->\n        <!--android:orientation=\"vertical\"-->\n        <!--android:layout_width=\"0dp\"-->\n        <!--android:layout_weight=\"1\"-->\n        <!--android:layout_height=\"match_parent\">-->\n        <!--<TextView-->\n            <!--android:id=\"@+id/setting_item_title\"-->\n            <!--android:layout_width=\"60dp\"-->\n            <!--android:layout_height=\"40dp\"-->\n            <!--android:gravity=\"center_vertical\"-->\n            <!--android:textSize=\"14sp\"-->\n            <!--android:textColor=\"@color/blackDomain\"-->\n            <!--tools:text=\"标题\" />-->\n        <!--<TextView-->\n            <!--android:visibility=\"gone\"-->\n            <!--android:id=\"@+id/setting_item_desc\"-->\n            <!--android:layout_width=\"wrap_content\"-->\n            <!--android:layout_height=\"wrap_content\"-->\n            <!--tools:text=\"哈哈哈，这里是详情\"-->\n            <!--android:textSize=\"12sp\" />-->\n    <!--</LinearLayout>-->\n\n    <TextView\n        android:id=\"@+id/setting_item_title\"\n        android:layout_width=\"0dp\"\n        android:layout_weight=\"1\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"center_vertical\"\n        android:textSize=\"14sp\"\n        android:textColor=\"?attr/lv_item_title_color\"\n        tools:text=\"标题\" />\n\n    <TextView\n        android:id=\"@+id/setting_item_value\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:paddingStart=\"5dp\"\n        android:paddingEnd=\"5dp\"\n        android:gravity=\"center_vertical|end\"\n        android:layout_gravity=\"center_vertical|end\"\n        tools:text=\"值\"\n        android:textSize=\"14sp\"\n        android:textColor=\"?attr/lv_item_title_color\"\n        android:drawableEnd=\"@drawable/ic_arrow_right\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/setting_item_session.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/setting_item\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"40dp\"\n    android:gravity=\"center_vertical\"\n    android:background=\"?android:selectableItemBackground\"\n    android:padding=\"10dp\"\n    android:baselineAligned=\"false\" android:orientation=\"vertical\">\n\n    <TextView\n        android:id=\"@+id/setting_session_title\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"center_vertical\"\n        android:textSize=\"13sp\"\n        android:textColor=\"?attr/lv_item_title_color\"\n        tools:text=\"标题\" />\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/setting_item_switch.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/setting_item\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"60dp\"\n    android:gravity=\"center_vertical\"\n    android:background=\"?android:selectableItemBackground\"\n    android:padding=\"10dp\"\n    android:baselineAligned=\"false\">\n\n    <TextView\n        android:id=\"@+id/setting_item_title\"\n        android:layout_width=\"0dp\"\n        android:layout_weight=\"1\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"center_vertical\"\n        android:textSize=\"14sp\"\n        android:textColor=\"?attr/lv_item_title_color\"\n        tools:text=\"标题\" />\n\n    <com.kyleduo.switchbutton.SwitchButton\n        android:id=\"@+id/setting_item_switch\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"5dp\"\n        android:gravity=\"center_vertical|end\"\n        android:layout_gravity=\"center_vertical|end\" />\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/tag_expandable_item_child.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:tools=\"http://schemas.android.com/tools\"\n        android:id=\"@+id/child_item\"\n        android:orientation=\"horizontal\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center_vertical\"\n        android:background=\"?attr/root_view_bg\"\n        tools:ShowIn=\"@layout/activity_main\">\n\n        <TextView\n            android:id=\"@+id/child_item_title\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:paddingTop=\"15dp\"\n            android:paddingBottom=\"15dp\"\n            android:paddingStart=\"70dp\"\n            android:layout_weight=\"1\"\n            android:gravity=\"center_vertical\"\n            android:lines=\"1\"\n            android:textColor=\"?attr/lv_item_title_color\"\n            android:textSize=\"16sp\"\n            tools:text=\"这是父item\" />\n\n        <!--android:textColor=\"#5a8fca\"-->\n\n        <TextView\n            android:id=\"@+id/child_item_count\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center\"\n            android:layout_marginEnd=\"20dp\"\n            android:textColor=\"?attr/lv_item_desc_color\"\n            android:textSize=\"12sp\"\n            tools:text=\"-9.9\" />\n    </LinearLayout>\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/tag_expandable_item_group.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:tag=\"sticky\">\n\n    <LinearLayout\n        android:id=\"@+id/group_item\"\n        android:orientation=\"horizontal\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center_vertical\"\n        android:background=\"?attr/root_view_bg\"\n        tools:ShowIn=\"@layout/activity_main\">\n\n        <me.wizos.loread.view.IconFontView\n            android:id=\"@+id/group_item_icon\"\n            android:layout_width=\"60dp\"\n            android:layout_height=\"match_parent\"\n            android:paddingTop=\"15dp\"\n            android:paddingBottom=\"15dp\"\n            android:paddingStart=\"20dp\"\n            android:paddingEnd=\"15dp\"\n            android:gravity=\"center\"\n            android:textSize=\"16sp\"\n            android:textColor=\"?attr/tag_slv_item_icon\"\n            android:text=\"@string/font_tag\" />\n\n\n        <TextView\n            android:id=\"@+id/group_item_title\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:paddingTop=\"15dp\"\n            android:paddingBottom=\"15dp\"\n            android:layout_weight=\"1\"\n            android:gravity=\"center_vertical\"\n            android:lines=\"1\"\n            android:textSize=\"16sp\"\n            android:textStyle=\"bold\"\n            android:textColor=\"?attr/lv_item_title_color\"\n            tools:text=\"这是父item\" />\n\n        <!--android:textColor=\"#5a8fca\"-->\n        <TextView\n            android:id=\"@+id/group_item_count\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center\"\n            android:layout_marginEnd=\"20dp\"\n            android:textColor=\"?attr/lv_item_desc_color\"\n            android:textSize=\"12sp\"\n            tools:text=\"1024\" />\n\n    </LinearLayout>\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/webview_long_clicked_popwindow.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"#2F2F2F\"\n    android:orientation=\"vertical\">\n\n    <TextView\n        android:id=\"@+id/webview_open_mode\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"40dp\"\n        android:gravity=\"center\"\n        android:background=\"?android:attr/selectableItemBackground\"\n        android:text=\"@string/open_mode\"\n        android:textColor=\"@android:color/white\"\n        android:textSize=\"16sp\" />\n    <TextView\n        android:id=\"@+id/webview_copy_link\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"40dp\"\n        android:gravity=\"center\"\n        android:background=\"?android:attr/selectableItemBackground\"\n        android:text=\"@string/copy_link\"\n        android:textColor=\"@android:color/white\"\n        android:textSize=\"16sp\" />\n    <TextView\n        android:id=\"@+id/webview_share_link\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"40dp\"\n        android:gravity=\"center\"\n        android:background=\"?android:attr/selectableItemBackground\"\n        android:text=\"@string/share_link\"\n        android:textColor=\"@android:color/white\"\n        android:textSize=\"16sp\" />\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/menu/menu_article.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\".activity.ArticleActivity\">\n\n    <item\n        android:id=\"@+id/article_menu_feed\"\n        android:icon=\"@drawable/ic_eye\"\n        android:title=\"订阅源\"\n        android:enabled=\"true\"\n        android:visible=\"true\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/article_menu_speak\"\n        android:title=\"朗读\"\n        android:enabled=\"true\"\n        android:visible=\"true\"\n        app:showAsAction=\"never\" />\n    <item\n        android:id=\"@+id/article_menu_article_info\"\n        android:title=\"原始数据\"\n        android:enabled=\"true\"\n        android:visible=\"true\"\n        app:showAsAction=\"never\" />\n    <item\n        android:id=\"@+id/article_menu_edit_content\"\n        android:title=\"编辑内容\"\n        android:enabled=\"true\"\n        android:visible=\"true\"\n        app:showAsAction=\"never\" />\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_web.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\".activity.WebActivity\">\n\n    <item\n        android:id=\"@+id/web_menu_refresh\"\n        android:icon=\"@drawable/ic_refresh\"\n        android:title=\"@string/refresh\"\n        android:enabled=\"true\"\n        android:visible=\"false\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/web_menu_stop\"\n        android:icon=\"@drawable/ic_stop_loading\"\n        android:title=\"@string/stop\"\n        android:enabled=\"true\"\n        android:visible=\"true\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/web_menu_user_agent\"\n        android:title=\"@string/ua_sign\"\n        android:icon=\"@drawable/ic_user_agent\"\n        android:enabled=\"true\"\n        android:visible=\"true\"\n        app:showAsAction=\"never\" />\n    <item\n        android:id=\"@+id/web_menu_open_by_sys\"\n        android:title=\"@string/open_by_browser\"\n        android:icon=\"@drawable/ic_browser\"\n        android:enabled=\"true\"\n        android:visible=\"true\"\n        app:showAsAction=\"never\" />\n    <item\n        android:id=\"@+id/web_menu_copy_link\"\n        android:icon=\"@drawable/ic_copy_link\"\n        android:title=\"@string/copy_link\"\n        android:enabled=\"true\"\n        android:visible=\"true\"\n        app:showAsAction=\"never\" />\n    <item\n        android:id=\"@+id/web_menu_share\"\n        android:icon=\"@drawable/ic_share\"\n        android:title=\"@string/share\"\n        android:enabled=\"true\"\n        android:visible=\"true\"\n        app:showAsAction=\"never\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/values/arrays.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <array name=\"theme_colors\">\n        <!--<item>#F44336</item>-->\n        <!--<item>#E91E63</item>-->\n        <!--<item>#9C27B0</item>-->\n        <item>@color/white</item>\n        <item>@color/dark_background</item>\n    </array>\n    <integer-array name=\"setting_clear_day_dialog_item_array\">\n        <item>0</item>\n        <item>1</item>\n        <item>3</item>\n        <item>7</item>\n        <item>15</item>\n        <item>30</item>\n    </integer-array>\n\n    <integer-array name=\"setting_sync_frequency_minute\">\n        <item>15</item>\n        <item>30</item>\n        <item>60</item>\n        <item>180</item>\n        <item>360</item>\n        <item>720</item>\n    </integer-array>\n\n    <integer-array name=\"display_mode\">\n        <item>0</item>\n        <item>1</item>\n        <item>2</item>\n    </integer-array>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/attr.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n\n    <attr name=\"status_bar\" format=\"reference|color\"/>\n    <attr name=\"root_view_bg\" format=\"reference|color\" />\n\n    <attr name=\"topbar_bg\" format=\"reference|color\" />\n    <attr name=\"topbar_fg\" format=\"reference|color\" />\n\n    <attr name=\"bottombar_bg\" format=\"reference|color\" />\n    <attr name=\"bottombar_fg\" format=\"reference|color\" />\n\n    <attr name=\"lv_item_bg\" format=\"reference|color\" />\n    <attr name=\"lv_item_fg\" format=\"reference|color\" />\n    <attr name=\"lv_item_title_color\" format=\"reference|color\" />\n    <attr name=\"setting_header_title_color\" format=\"reference|color\" />\n    <attr name=\"lv_item_desc_color\" format=\"reference|color\" />\n    <attr name=\"lv_item_info_color\" format=\"reference|color\" />\n    <attr name=\"lv_item_divider\" format=\"reference|color\"/>\n\n    <attr name=\"art_title\" format=\"reference|color\"/>\n    <attr name=\"art_feed\" format=\"reference|color\"/>\n    <attr name=\"art_date\" format=\"reference|color\"/>\n    <attr name=\"art_time\" format=\"reference|color\"/>\n\n    <attr name=\"icon_title_color\" format=\"reference|color\" />\n\n    <attr name=\"setting_title\" format=\"reference|color\"/>\n    <attr name=\"setting_tips\" format=\"reference|color\"/>\n\n    <attr name=\"tag_slv_item_icon\" format=\"reference|color\"/>\n\n    <attr name=\"bottombar_divider\" format=\"reference|color\" />\n\n    <attr name=\"bubble_bg\" format=\"reference\" />\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--放入自定义的属性-->\n<resources>\n\n    <attr name=\"icon_text\" format=\"string\" />\n    <attr name=\"icon_color\" format=\"color\" />\n    <attr name=\"icon_size\" format=\"dimension\" />\n    <attr name=\"title_color\" format=\"color\" />\n    <attr name=\"title_text\" format=\"string\" />\n    <attr name=\"title_size\" format=\"dimension\" />\n\n\n    <!--为自定义控件TabIconView做的自定义布局属性-->\n\n\n    <declare-styleable name=\"IconView\">\n        <attr name=\"icon_color\" />\n        <attr name=\"icon_text\" />\n        <attr name=\"icon_size\" />\n        <attr name=\"title_color\" />\n        <attr name=\"title_text\" />\n        <attr name=\"title_size\" />\n    </declare-styleable>\n\n    <!-- Declare custom theme attributes that allow changing which styles are\n         used for button bars dependcheckedg on the API level.\n         ?android:attr/buttonBarStyle is new as of API 11 so this is\n         necessary to support previous API levels. -->\n    <declare-styleable name=\"ButtonBarContainerTheme\">\n        <attr name=\"metaButtonBarStyle\" format=\"reference\" />\n        <attr name=\"metaButtonBarButtonStyle\" format=\"reference\" />\n    </declare-styleable>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#3F51B5</color>\n    <color name=\"colorPrimaryDark\">#303F9F</color>\n    <color name=\"primary\">#4183C4</color>\n\n\n    <color name=\"bluePrimary\">#28A4E9</color>\n    <color name=\"colorAccent\">#FF4081</color>\n    <color name=\"crimson\">#e62e2e</color>\n\n    <color name=\"white\">#FFFFFF</color>\n\n\n    <color name=\"main_grey_dark\">#E3E8E7</color>\n\n    <color name=\"main_day_light\">#515151</color>\n    <!--<couserNamename=\"main_day_dark\">#313131</color>-->\n    <color name=\"main_day_dark\">#343432</color>\n    <!--<couserNamename=\"main_day_info\">#7c7c7c</color>-->\n    <color name=\"main_day_info\">#a9a9a9</color>\n\n    <!--<couserNamename=\"main_night_dark\">#A3AAB0</color>-->\n    // 7B9196\n    <color name=\"light_lv_divider\">#f2f2f2</color>\n\n    <!--Reeder的配色，不过在我手机上显示效果不好-->\n    <color name=\"main_night_title\">#E8E6E8</color>\n    <!--<couserNamename=\"main_night_info\">#959492</color>-->\n    <!--<couserNamename=\"dark_background\">#4B4A47</color>-->\n    <!--<couserNamename=\"dark_foreground\">#525252</color>-->\n    <!--<couserNamename=\"black_lv_divider\">#3E3D3B</color>-->\n    <!--NewsFlow的配色 #b5b5b5-->\n    <color name=\"main_night_info\">#b5b5b5</color>\n    <color name=\"dark_background\">#454952</color>\n    <color name=\"dark_foreground\">#545863</color>\n    <color name=\"black_lv_divider\">#3d3d3d</color>\n    <!--我的原始配色-->\n    <!--<couserNamename=\"main_night_info\">#7B9196</color>-->\n    <!--<couserNamename=\"dark_background\">#202B2F</color>-->\n    <!--<couserNamename=\"dark_foreground\">#203238</color>-->\n    <!--<couserNamename=\"black_lv_divider\">#141e21</color>-->\n\n\n    <color name=\"material_red_400\">#EF5350</color>\n    <color name=\"material_teal_a400\">#1DE9B6</color>\n\n    // 测试一个主题色 EFF3F2\n    <color name=\"white_toolbar_bg\">#FCFBF7</color>\n    <color name=\"white_screen_bg\">#F6F5F1</color>\n    <color name=\"white_bottombar_divider\">#e7e7e7</color>\n\n    <color name=\"shade\">#404040</color>\n\n    <color name=\"placeholder_bg\">#E6E7E7</color>\n\n    <color name=\"black_overlay\">#66000000</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n    <dimen name=\"activity_vertical_margin\">16dp</dimen>\n    <dimen name=\"fab_margin\">16dp</dimen>\n    <dimen name=\"slv_item_height\">140dip</dimen>\n    <dimen name=\"txt_size\">8sp</dimen>\n    <dimen name=\"slv_menu_left_width\">36dp</dimen>\n    <dimen name=\"slv_menu_right_width\">36dp</dimen>\n    <dimen name=\"bottom_bar_height\">40dp</dimen>\n    <dimen name=\"dp_80\">80dp</dimen>\n    <dimen name=\"dp_30\">30dp</dimen>\n    <dimen name=\"md_colorchooser_circlesize\">40dp</dimen>\n    <dimen name=\"app_bar_height\">180dp</dimen>\n    <dimen name=\"text_margin\">16dp</dimen>\n    <dimen name=\"design_fab_size_normal\" tools:ignore=\"PrivateResource\">40dp</dimen>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_id\">loread</string>\n    <string name=\"app_name\">Loread</string>\n    <string name=\"settings\">Settings</string>\n    <string name=\"category\">Category</string>\n    <string name=\"un_category\">UnCategory</string>\n    <string name=\"tag\">Tag</string>\n    <string name=\"edit_category\">Edit Category</string>\n    <string name=\"select_category\">Select Category</string>\n    <string name=\"approaching_the_world\">Approaching the World</string>\n\n    <string name=\"select_tag\">Select tag</string>\n\n    <string name=\"custom\">Custom</string>\n\n    <string name=\"select_user_agent\">Select UserAgent</string>\n    <string name=\"custom_user_agent\">Custom UserAgent</string>\n    <string name=\"enter_user_agent\">Enter UserAgent</string>\n    <string name=\"remove_custom_user_agent\">Remove custom UserAgent</string>\n    <string name=\"select_display_mode\">Select the default display mode for articles</string>\n\n    <string name=\"theme_switched_automatically\">Themes have been switched automatically</string>\n    <string name=\"feed_url_display\">Feed URL: %s</string>\n    <string name=\"site_url_display\">Site URL: %s</string>\n    <string name=\"site_remark\">Site Remark</string>\n    <string name=\"remark\">Remark</string>\n    <string name=\"save_directory\">Directory of articles saved</string>\n    <string name=\"default_directory\">Default directory</string>\n    <string name=\"root_directory\">Root_directory</string>\n    <string name=\"edit_directory\">Edit directory</string>\n    <string name=\"new_directory\">New directory</string>\n    <string name=\"feed_title_as_directory\">Title of the feed as directory</string>\n    <string name=\"category_title_as_directory\">Title of the categories as directory</string>\n    <string name=\"custom_save_directory\">Custom save directory</string>\n    <string name=\"saved_to_root_directory\">Saved to root directory</string>\n    <string name=\"saved_to_directory\">Saved to \"%1$s\" directory</string>\n\n    <string name=\"star_marked\">Star marked</string>\n    <string name=\"star_marked_to_favorites\">Star marked to \"%1$s\" favorites</string>\n\n    <string name=\"new_favorites\">New favorites</string>\n    <string name=\"add_to_favorites\">Add to favorites</string>\n    <string name=\"edit_favorites\">Edit favorites</string>\n\n    <string name=\"etc\">, etc</string>\n\n\n    <string name=\"default_x\">Default</string>\n    <string name=\"music\">Music</string>\n    <string name=\"speak\">Speak</string>\n    <string name=\"auth_username\">Username</string>\n    <string name=\"auth_password\">Password</string>\n    <string name=\"login\">Login</string>\n    <string name=\"please_input_account_id\">Please enter account id</string>\n    <string name=\"please_input_account_pw\">Please enter account password</string>\n    <string name=\"no_account_register_now\">No account?  Register now!</string>\n    <string name=\"do_you_want_to_download_files\">Do you want to download the file?</string>\n\n    <string name=\"plz_grant_permission_tips\">Please grant permission, otherwise the APP may not work properly!</string>\n    <string name=\"authing\">Authorizing</string>\n    <string name=\"fetch_user_info\">Getting Basic Information</string>\n    <string name=\"auth_failure_please_try_again\">Authorization failed, please try again.</string>\n    <string name=\"login_please_wait\">Signing in</string>\n    <string name=\"login_failed_reason\">Login failed, reason: %1$s</string>\n    <string name=\"login_failure_please_try_again\">Login failed, please try again</string>\n    <string name=\"login_failure_please_check_account\">Login failed, please check account and password</string>\n    <string name=\"temporarily_not_supported\">Temporarily not supported</string>\n\n    <!-- Strings related to Settings -->\n    <string name=\"main_toolbar_title_search\">Search: </string>\n\n\n    <plurals name=\"search_result_followers\">\n        <item quantity=\"one\">%1$d follower</item>\n        <item quantity=\"other\">%1$d followers</item>\n    </plurals>\n\n    <string name=\"search_result_articles\"> | Update %1$f articles per week</string>\n\n    <string name=\"search_result_summary\">Intro: %1$s | Feed URL: %2$s</string>\n    <string name=\"search_result_subs\">%1$d followers | Update %2$s articles per week</string>\n    <string name=\"search_result_last_update_time\">Last update %1$s</string>\n\n    <string name=\"do_you_want_to_jump_the_application\">Do you want to jump to \"%1$s\" application?</string>\n    <string name=\"corresponding\">Corresponding</string>\n    <string name=\"unable_to_find_app\">No corresponding application found</string>\n\n\n    <!--下面这个还没有用到-->\n    <string name=\"sync_article_content\">1. Sync article content [%1$d / %2$d]</string>\n    <string name=\"sync_article_refs\">2. Sync Article Info</string>\n    <string name=\"sync_feed_info\">3. Sync feed info</string>\n\n    <string name=\"fetch_article_full_content\">Fetch article full content</string>\n\n    <plurals name=\"articles_count\">\n        <item quantity=\"one\">%1$s article</item>\n        <item quantity=\"other\">%1$s articles</item>\n    </plurals>\n\n    <plurals name=\"has_new_articles\">\n        <item quantity=\"one\">Has %1$s new article</item>\n        <item quantity=\"other\">Has %1$s new articles</item>\n    </plurals>\n\n    <string name=\"share\">Share</string>\n    <string name=\"share_to\">Share to</string>\n    <string name=\"warning\">Warning</string>\n    <string name=\"agree\">Agree</string>\n    <string name=\"disagree\">Disagree</string>\n    <string name=\"confirm\">Confirm</string>\n    <string name=\"cancel\">Cancel</string>\n    <string name=\"ua_sign\">UA</string>\n\n    <string name=\"edit_success\">Edit successful</string>\n    <string name=\"edit_fail\">Edit fail</string>\n    <string name=\"editing\">Editing</string>\n\n    <string name=\"mask_success\">Mask successful</string>\n    <string name=\"mask_fail\">Mask fail</string>\n\n    <string name=\"main_dialog_esc_positive\">YES ^_^</string>\n    <string name=\"main_dialog_esc_negative\">NO ！</string>\n\n    <string name=\"are_you_sure_that_unsubscribe_this_feed_link\">Are you sure you want to unsubscribe from this site?</string>\n    <string name=\"main_dialog_confirm_mark_article_list\">Are you sure you want to mark the following articles as read?</string>\n    <string name=\"main_dialog_esc_confirm\">Are you sure you want to exit the app?</string>\n    <string name=\"setting_about_dialog_title\">Sigh with emotion</string>\n    <string name=\"setting_about_dialog_content\">路很长，纵然远望，却不知方向。\\n抽支烟，思绪无常，奔跑着彷徨。\\n逃不脱的苟且，到不了的远方…\\n作于2016.10.07</string>\n\n\n    <string name=\"article_choose_tag_dialog_title\">Select label</string>\n\n\n    <!--数据同步-->\n    <string name=\"sync\">Sync</string>\n    <string name=\"setting_auto_sync_title\">Auto sync</string>\n    <string name=\"setting_auto_sync_on_wifi_title\">Auto sync in Wifi mode</string>\n    <string name=\"setting_sync_frequency_title\">Time interval for sync</string>\n\n    <string name=\"setting_down_img_title\">Save traffic to download pictures</string>\n    <string name=\"proxy\">Proxy</string>\n    <!--<struserNamename=\"setting_inoreader_proxy_dialog_title\">设置代理地址</string>-->\n\n    <!--界面显示-->\n    <string name=\"setting_auto_toggle_theme_title\">Auto switch night theme</string>\n\n    <string name=\"other\">Other</string>\n    <!--操作-->\n    <string name=\"open_link_by_system_browser\">Open link by system browser</string>\n    <!--缓存-->\n    <string name=\"setting_clear_day_title\">Retention period of read articles</string>\n    <!--<struserNamename=\"setting_clear_day_dialog_title\">选择保留最近多少天的文章呢？</string>-->\n    <string name=\"clear_article\">Clean up articles</string>\n    <string name=\"handle_crawldate\">Handle crawldate</string>\n\n    <!--备份-->\n    <string name=\"backup_now\">Back up now</string>\n    <string name=\"restore_backup\">Restore backup</string>\n    <string name=\"handle_saved_articles\">Organize saved articles</string>\n\n    <string name=\"loading\">Loading</string>\n    <string name=\"upgrading\">Upgrading</string>\n    <string name=\"upgrading_please_wait_a_moment\">Upgrading, please wait a moment</string>\n    <string name=\"https_scheme\">https://</string>\n    <string name=\"inoreader_url\">https://inoreader.com</string>\n    <!--关于-->\n    <string name=\"about\">About</string>\n    <string name=\"license\">License</string>\n    <string name=\"feedback\">Feedback</string>\n    <string name=\"qq_group\">QQ group</string>\n    <string name=\"qq_group_number\">106211435</string>\n    <string name=\"esc\">Quit</string>\n\n    <string name=\"add_account\">Add account</string>\n    <string name=\"esc_account\">Logout</string>\n    <string name=\"switch_account\">Switch account</string>\n\n    <string name=\"article_info\">Information of the article</string>\n\n    <string name=\"config_the_feed\">Config this feed</string>\n    <string name=\"theme_setting\">Theme settings</string>\n\n    <string name=\"tips_no_net\">Network is\\'t connected, please check it</string>\n    <string name=\"bag_network\">Network is not good, please try again</string>\n    <string name=\"fail_try\">Failed, please try again</string>\n\n    <!--<string name=\"tag_choose_tips\">想看哪个分类？</string>-->\n\n    <string name=\"xx_minute\">%1$s minutes</string>\n    <string name=\"xx_hour\">%1$s hours</string>\n    <string name=\"clear_day_summary\">%1$s days</string>\n\n    <string name=\"save_img\">Save picture</string>\n    <string name=\"share_img\">Share picture</string>\n    <string name=\"rename\">Rename</string>\n\n    <string name=\"mark_up\">Mark Read Up</string>\n    <string name=\"mark_down\">Mark Read Down</string>\n    <string name=\"mark_unread\">Mark unread</string>\n    <string name=\"speak_article\">Speak article</string>\n\n\n    <string name=\"save_readability_content\">Save Readability content?</string>\n    <string name=\"get_readability_ing\">Getting</string>\n    <string name=\"get_readability_success\">Get Successful</string>\n    <string name=\"get_readability_failure\">Get failed</string>\n    <string name=\"cancel_readability\">The full text obtained has been cancelled</string>\n\n    <string name=\"music_time\">0:00</string>\n    <string name=\"music_speed\">1.0</string>\n\n    <string name=\"edit_name\">Edit name</string>\n    <string name=\"search\">Search</string>\n    <string name=\"search_key\">Input key words</string>\n\n    <string name=\"search_cloudy_feeds_result_count\">%1$d feeds found</string>\n    <string name=\"search_header_search_cloudy_feeds\">Search feeds</string>\n    <string name=\"search_header_search_local_articles\">Search articles on my phone</string>\n\n    <string name=\"share_link\">Copy link</string>\n    <string name=\"copy_link\">Share link</string>\n    <string name=\"open_mode\">Open mode</string>\n    <string name=\"copy_success\">Copy succeeded</string>\n\n    <string name=\"please_input_keyword\">please enter keyword</string>\n\n    <string name=\"subscribe\">Subscribe</string>\n    <string name=\"subscribe_success\">Subscription succeeded, please sync manually</string>\n    <string name=\"subscribe_fail\">Subscription failed, please try again</string>\n    <string name=\"unsubscribe_succeeded\">Unsubscribe succeeded</string>\n    <string name=\"unsubscribe_failed\">Unsubscribe failed: %1$s</string>\n    <string name=\"rename_failed\">Rename failed, please try again</string>\n\n    <string name=\"no_title\">Untitled</string>\n    <string name=\"no_thing\">No</string>\n\n    <string name=\"more_setting\">More settings</string>\n    <string name=\"auto_mark_when_scrolling\">Mark as read when scrolling</string>\n    <string name=\"down_img_on_wifi\">Download images only when connecting Wifi</string>\n    <string name=\"article_list_state\">Filter articles</string>\n    <string name=\"include_valueless\">Include Valueless</string>\n    <string name=\"night_theme\">Night theme</string>\n\n    <string name=\"article_list_order_asc\">Older first</string>\n    <string name=\"article_list_order_desc\">Newest first</string>\n\n\n    <string name=\"all\">All</string>\n    <string name=\"unread\">Unread</string>\n    <string name=\"starred\">Star</string>\n\n    <string name=\"rss\">RSS</string>\n    <string name=\"readability\">Readability</string>\n    <string name=\"original\">Original</string>\n\n    <string name=\"view\">View</string>\n    <string name=\"open_original_article\">Open original article</string>\n\n    <string name=\"refresh\">Refresh</string>\n    <string name=\"stop\">Stop</string>\n    <string name=\"open_by_browser\">Open by browser</string>\n    <string name=\"download_file_position\">Download to directory: download</string>\n    <string name=\"file_size\">File size: %1$s</string>\n    <string name=\"unknown\">Unknown</string>\n\n\n    <string name=\"wrong_username_or_password\">Incorrect username or password</string>\n\n    <string name=\"wrong_unknown\">Unknown error</string>\n\n    <string name=\"is_filed_cannot_edit\">The article has been exported and cannot be modified</string>\n\n    <string name=\"font_unstar\">&#xe64c;</string>\n    <string name=\"font_stared\">&#xe64b;</string>\n    <string name=\"font_readed\">&#xe72f;</string>\n    <string name=\"font_unread\">&#xe656;</string>\n\n    <string name=\"font_setting\">&#xe61f;</string>\n    <string name=\"font_theme\">&#xe650;</string>\n\n    <string name=\"font_unsave\">&#xe604;</string>\n    <string name=\"font_saved\">&#xe605;</string>\n    <string name=\"font_main\">&#xe60c;</string>\n\n    <string name=\"font_add\">&#xe699;</string>\n    <string name=\"font_cross\">&#xe6cb;</string>\n    <string name=\"font_tick\">&#xe636;</string>\n\n\n    <string name=\"font_group\">&#xe615;</string>\n\n    <string name=\"font_tag\">&#xe7c4;</string>\n    <string name=\"font_label\">&#xe7c5;</string>\n\n    <string name=\"font_arrow_down\">&#xe6a6;</string>\n    <string name=\"font_arrow_right\">&#xe6a8;</string>\n    <string name=\"font_arrow_left\">&#xe84e;</string>\n\n    <string name=\"font_list_reading\">&#xe612;</string>\n    <string name=\"font_list_unread\">&#xe613;</string>\n\n\n    <string name=\"font_no_data\">&#xe608;</string>\n    <string name=\"font_search\">&#xe741;</string>\n\n\n    <string name=\"font_share\">&#xe616;</string>\n    <string name=\"font_more\">&#xe60f;</string>\n\n    <string name=\"font_archive\">&#xe680;</string>\n\n    <string name=\"font_translation\">&#xe6ba;</string>\n    <string name=\"font_chrome\">&#xe678;</string>\n    <string name=\"font_book\">&#xe65b;</string>\n    <string name=\"font_article_original\" translatable=\"false\">&#xe61b;</string>\n    <string name=\"font_article_readability\" translatable=\"false\">&#xe743;</string>\n    <string name=\"font_readability\">&#xe611;</string>\n    <string name=\"font_feed_config\">&#xe72b;</string>\n    <string name=\"font_update\">&#xe645;</string>\n\n\n    <!--准备删除-->\n    <string name=\"font_last\">&#xe65b;</string>\n    <string name=\"font_next\">&#xe6a7;</string>\n    <string name=\"font_all_article\">&#xe6a7;</string>\n    <string name=\"font_list_stared\">&#xe617;</string>\n    <string name=\"font_list_unstar\">&#xe614;</string>\n\n    <!--气泡-->\n    <string name=\"font_untag_article\">&#xe84d;</string>\n\n\n    <string name=\"title_activity_scrolling\">ScrollingActivity</string>\n\n    <string name=\"action_settings\">Settings</string>\n    <string name=\"server_host\">Server Host</string>\n    <string name=\"account\">Account</string>\n    <string name=\"password\">Password</string>\n    <string name=\"welcome\">\"Welcome!\"</string>\n    <string name=\"invalid_username\">Invalid username</string>\n    <string name=\"invalid_host\">Invalid Server Host, like https://example.com</string>\n    <string name=\"empty_host\">Empty Server Host, please reset it</string>\n\n    <string name=\"invalid_password\">Password must be >5 characters</string>\n\n    <string name=\"article_title_is\">article title is: </string>\n\n    <string name=\"summary_pre\">[Here is a pre]</string>\n    <string name=\"summary_table\">[Here is a table]</string>\n    <string name=\"summary_link\">[Here is a link]</string>\n    <string name=\"summary_frame\">[Here is a frame]</string>\n    <string name=\"summary_audio\">[Here is a audio]</string>\n    <string name=\"summary_video\">[Here is a video]</string>\n    <string name=\"summary_image\">[Here is a image]</string>\n\n\n    <string name=\"frame\">[frame]</string>\n    <string name=\"audio\">[audio]</string>\n    <string name=\"video\">[video]</string>\n    <string name=\"image\">[image]</string>\n\n    <string name=\"inoreader_oauth\">InoReader (OAuth)</string>\n    <string name=\"inoreader_login\">InoReader (Login)</string>\n    <string name=\"feedly\">Feedly</string>\n    <string name=\"tinytinyrss\">TinyTinyRSS</string>\n    <string name=\"intro\">Intro</string>\n\n    <string name=\"everything_is_rssible\">Everything is RSSible</string>\n\n    <string name=\"developed_by_wizos\">Developed by Wizos</string>\n\n    <string name=\"do_you_want_to_delete_data_of_this_account_after_logout\">Do you want to delete all data of this account after logout?</string>\n    <string name=\"loread_plugin_must_be_installed\">Note: Loread plugin must be installed in \\'plugins.local\\' directory</string>\n    <string name=\"the_server_host_like\">The server host like \"https://example.com\"</string>\n    <string name=\"rss_summary\">\n        In layman\\'s terms, RSS is a way to subscribe to information.\\n\\n\n        Just like following John Doe on Twitter and Jane Doe on TikTok, you can follow them by subscribe for a FEED URL in the \"RSS Reader\".\\n\\n\n        This allows you to subscribe to Youtube, Instagram, Quora, Blog, Forum …</string>\n    <string name=\"authorizing\">Authorizing…</string>\n\n    <string name=\"open_with\">Open with</string>\n\n\n    <string name=\"feed_desc\">Description: </string>\n    <string name=\"site_url\">Site url: </string>\n    <string name=\"feed_url\">Feed url: </string>\n    <string name=\"unsubscribe\">Unsubscribe</string>\n\n    <string name=\"display_filter_in_development\">\\\"Display Filter\\\", in development</string>\n\n    <string name=\"click_to_load_this_picture\">Click to load this picture</string>\n    <string name=\"loading_failed_click_here_to_retry\">Loading failed! Click here to retry</string>\n    <string name=\"picture_error_click_here_to_retry\">Picture error! Click here to retry</string>\n    <string name=\"copied\">Copied</string>\n    \n    <string name=\"the_rule_of_full_text_extraction_has_expired\">The rule of full-text extraction has expired: %1$s</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <style name=\"AppTheme.AppBarOverlay\" parent=\"ThemeOverlay.AppCompat.Dark.ActionBar\" />\n\n    <style name=\"AppTheme.PopupOverlay\" parent=\"ThemeOverlay.AppCompat.Light\" />\n\n    <!--Theme.AppCompat.Light.DarkActionBar-->\n    <!--QMUI.Compat.NoActionBar-->\n    <style name=\"AppBaseTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <item name=\"windowActionBar\">false</item>\n        <item name=\"windowNoTitle\">true</item>\n        <item name=\"toolbarStyle\">@style/MyToolbar</item>\n        <item name=\"android:windowContentTransitions\">true</item>\n        <item name=\"toolbarNavigationButtonStyle\">@style/MyToolbar.Navigation</item>\n        <item name=\"coordinatorLayoutStyle\">@style/Widget.Support.CoordinatorLayout</item>\n        <item name=\"floatingActionButtonStyle\">@style/Widget.Design.FloatingActionButton</item>\n        <!--<item name=\"floatingActionButtonStyle\">@style/Widget.Support.FloatingActionButton</item>-->\n        <!--去掉toolbar下的阴影-->\n        <!--<iuserNamename=\"elevation\">0dp</item>-->\n\n        <!--<iuserNamename=\"app:subtitleTextAppearance\">@style/MyToolbar.SubTitle</item>-->\n        <!--app:subtitleTextAppearance=\"@style/toolbar_subtitle_style\"-->\n        <!--<iuserNamename=\"android:windowDrawsSystemBarBackgrounds\">true</item>-->\n        <!--<iuserNamename=\"android:statusBarColor\">@android:color/transparent</item>-->\n        <!--<iuserNamename=\"android:fitsSystemWindows\">true</item>-->\n        <!--<iuserNamename=\"android:addStatesFromChildren\">true</item>-->\n        <!--<iuserNamename=\"android:windowBackground\">@null</item>-->\n    </style>\n\n    <style name=\"AppBaseTheme.SplashTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <item name=\"android:windowBackground\">@drawable/splash_layers</item>\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n    </style>\n\n    <style name=\"AppBaseTheme.SplashTranslucentTheme\" parent=\"AppTheme.Day.NoActionBar\">\n        <item name=\"android:windowBackground\">@drawable/splash_layers</item>\n        <!--<iuserNamename=\"android:windowBackground\">@android:color/transparent</item>-->\n        <!--<iuserNamename=\"android:windowIsTranslucent\">true</item>-->\n    </style>\n\n    <style name=\"MyToolbar\" parent=\"Widget.AppCompat.Toolbar\">\n        <item name=\"contentInsetStart\">0dp</item>\n    </style>\n\n    <style name=\"MyToolbar.Navigation\" parent=\"Widget.AppCompat.Toolbar.Button.Navigation\">\n        <item name=\"android:minWidth\">40dp</item>\n        <item name=\"android:scaleType\">center</item>\n    </style>\n\n    <!--toolbar副标题样式-->\n    <style name=\"MyToolbar.ExpandedToolbarTitle\" parent=\"@style/TextAppearance.Widget.AppCompat.Toolbar.Title\">\n        <item name=\"android:textSize\">26sp</item>\n    </style>\n\n    <style name=\"MyToolbar.CollapsedToolbarTitle\" parent=\"@style/TextAppearance.Widget.AppCompat.Toolbar.Title\">\n        <item name=\"android:textSize\">15sp</item>\n    </style>\n\n    <style name=\"MyToolbar.SubTitle\" parent=\"@style/TextAppearance.Widget.AppCompat.Toolbar.Subtitle\">\n        <item name=\"android:textSize\">10sp</item>\n        <item name=\"android:layout_margin\">0dp</item>\n        <item name=\"android:padding\">0dp</item>\n        <!--<iuserNamename=\"android:textColor\">@android:color/white</item>-->\n    </style>\n\n    <!--白天主题-->\n    <style name=\"AppTheme.Day\" parent=\"AppBaseTheme\">\n        <!-- Customize your theme here. -->\n        <item name=\"status_bar\">@color/main_day_dark</item>\n        <!--<iuserNamename=\"colorPrimary\">@color/main_day_dark</item>-->\n        <!--<iuserNamename=\"colorPrimaryDark\">@color/main_day_dark</item>-->\n        <item name=\"root_view_bg\">@color/white</item>\n        // white_screen_bg\n        <item name=\"topbar_bg\">@color/main_day_dark</item>\n        <item name=\"topbar_fg\">@color/white</item>\n\n        <item name=\"bottombar_divider\">@color/white_bottombar_divider</item>\n        // 测试新的淡雅主题\n\n        <item name=\"bottombar_bg\">@color/white</item>\n        <item name=\"bottombar_fg\">@color/main_day_light</item>\n\n        <item name=\"lv_item_bg\">@color/white_screen_bg</item>\n        <item name=\"lv_item_fg\">@color/main_day_dark</item>\n\n        <item name=\"tag_slv_item_icon\">@color/main_day_dark</item>\n\n        <item name=\"icon_title_color\">@color/main_day_dark</item>\n        <item name=\"lv_item_title_color\">@color/main_day_dark</item>\n        <item name=\"setting_header_title_color\">@color/main_day_dark</item>\n        <item name=\"lv_item_desc_color\">@color/main_day_info</item>\n        <item name=\"lv_item_info_color\">@color/main_day_info</item>\n        <item name=\"lv_item_divider\">@color/white</item>\n        // light_lv_divider\n\n        <item name=\"art_title\">@color/main_day_dark</item>\n        <item name=\"art_feed\">@color/main_day_light</item>\n        <item name=\"art_date\">@color/main_day_light</item>\n        <item name=\"art_time\">@color/main_day_light</item>\n\n        <item name=\"setting_title\">@color/main_day_dark</item>\n        <item name=\"setting_tips\">@color/main_day_info</item>\n\n        <!--<iuserNamename=\"bubble_bg\">@color/white_toolbar_bg</item>-->\n        <item name=\"bubble_bg\">@drawable/textview_border_day</item>\n    </style>\n\n\n    <!--夜间主题-->\n    <style name=\"AppTheme.Night\" parent=\"AppBaseTheme\">\n        <!-- Customize your theme here. -->\n        <item name=\"status_bar\">@color/dark_foreground</item>\n        <!--<iuserNamename=\"colorPrimary\">@color/dark_foreground</item>-->\n        <!--<iuserNamename=\"colorPrimaryDark\">@color/dark_foreground</item>-->\n        <item name=\"root_view_bg\">@color/dark_background</item>\n        <item name=\"topbar_bg\">@color/dark_foreground</item>\n        <item name=\"topbar_fg\">@color/main_night_title</item>\n\n        <item name=\"bottombar_divider\">@color/black_lv_divider</item>\n\n        <item name=\"bottombar_bg\">@color/dark_background</item>\n        <item name=\"bottombar_fg\">@color/main_night_title</item>\n\n        <item name=\"lv_item_bg\">@color/dark_background</item>\n        <item name=\"lv_item_fg\">@color/dark_foreground</item>\n\n        <item name=\"tag_slv_item_icon\">@color/main_night_title</item>\n        <item name=\"icon_title_color\">@color/main_night_info</item>\n        <item name=\"setting_header_title_color\">@color/main_night_info</item>\n        <item name=\"lv_item_title_color\">@color/main_night_title</item>\n        <item name=\"lv_item_desc_color\">@color/main_night_info</item>\n        // main_night_dark\n        <item name=\"lv_item_info_color\">@color/main_night_info</item>\n        <item name=\"lv_item_divider\">@color/dark_background</item>\n        // black_lv_divider\n\n        <item name=\"art_title\">#039586</item>\n        <item name=\"art_feed\">#057971</item>\n        <item name=\"art_date\">#057971</item>\n        <item name=\"art_time\">#057971</item>\n\n        <item name=\"setting_title\">@color/main_night_title</item>\n        <item name=\"setting_tips\">@color/main_night_info</item>\n\n        <item name=\"bubble_bg\">@drawable/textview_border_night</item>\n    </style>\n\n    <style name=\"AppTheme.Day.NoActionBar\">\n        <item name=\"windowActionBar\">false</item>\n        <item name=\"windowNoTitle\">true</item>\n    </style>\n\n\n    <style name=\"SettingItem\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:textColor\">?attr/lv_item_title_color</item>\n        <item name=\"android:padding\">10dp</item>\n    </style>\n\n    <style name=\"SettingItemTitle2\">\n        <item name=\"android:layout_width\">0dp</item>\n        <item name=\"android:layout_weight\">1</item>\n        <item name=\"android:layout_height\">45dp</item>\n        <item name=\"android:textSize\">16sp</item>\n        <item name=\"android:textColor\">?attr/lv_item_title_color</item>\n        <item name=\"android:layout_gravity\">center</item>\n        <item name=\"android:gravity\">center_vertical</item>\n    </style>\n\n    <style name=\"SwitchButtonStyle\">\n        <item name=\"android:paddingLeft\">10dp</item>\n        <item name=\"android:paddingRight\">10dp</item>\n        <item name=\"android:paddingTop\">8dp</item>\n        <item name=\"android:paddingBottom\">8dp</item>\n        <item name=\"android:layout_width\">wrap_content</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:layout_gravity\">center</item>\n        <item name=\"kswBackDrawable\">@drawable/flyme_style_switch_button_round</item>\n        <item name=\"kswThumbRangeRatio\">2.2</item>\n        <item name=\"kswThumbDrawable\">@drawable/flyme_style_switch_button_rectangle</item>\n        <item name=\"kswThumbHeight\">16dp</item>\n        <item name=\"kswThumbWidth\">16dp</item>\n    </style>\n\n    <style name=\"SettingItemArrowRight\">\n        <item name=\"android:layout_width\">0dp</item>\n        <item name=\"android:layout_weight\">1</item>\n        <item name=\"android:layout_height\">45dp</item>\n        <item name=\"android:textSize\">16sp</item>\n        <item name=\"android:textColor\">?attr/lv_item_title_color</item>\n        <item name=\"android:layout_gravity\">center</item>\n        <item name=\"android:gravity\">center_vertical</item>\n    </style>\n\n    <style name=\"SettingItemTitle\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">38dp</item>\n        <item name=\"android:textSize\">18sp</item>\n        <item name=\"android:textColor\">?attr/lv_item_title_color</item>\n        <item name=\"android:layout_gravity\">center</item>\n        <item name=\"android:gravity\">center_vertical</item>\n    </style>\n\n\n    <style name=\"BottomSheetItem\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:layout_margin\">5dp</item>\n    </style>\n\n\n    <style name=\"SettingItemHeader\">\n        <!--<iuserNamename=\"android:alpha\">0.80</item>-->\n        <item name=\"android:minWidth\">10dp</item>\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:textSize\">12sp</item>\n        <item name=\"android:textColor\">?attr/lv_item_desc_color</item>\n        <item name=\"android:gravity\">center|left</item>\n        <item name=\"android:padding\">10dp</item>\n    </style>\n\n    <style name=\"BottomSheetItemTitle\">\n        <item name=\"android:layout_width\">0dp</item>\n        <item name=\"android:layout_weight\">1</item>\n        <item name=\"android:layout_height\">38dp</item>\n        <item name=\"android:textSize\">14sp</item>\n        <item name=\"android:textColor\">?attr/lv_item_title_color</item>\n        <item name=\"android:layout_gravity\">center</item>\n        <item name=\"android:gravity\">center_vertical</item>\n    </style>\n\n    <style name=\"SettingItemTitleHorizontal\">\n        <item name=\"android:layout_width\">0dp</item>\n        <item name=\"android:layout_weight\">1</item>\n        <item name=\"android:layout_height\">38dp</item>\n        <item name=\"android:textSize\">18sp</item>\n        <item name=\"android:textColor\">?attr/lv_item_title_color</item>\n        <item name=\"android:layout_gravity\">center</item>\n        <item name=\"android:gravity\">center_vertical</item>\n    </style>\n\n    <style name=\"SettingItemTips\">\n        <!--<iuserNamename=\"android:alpha\">0.80</item>-->\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:layout_marginStart\">0dp</item>\n        <item name=\"android:textSize\">12sp</item>\n        <item name=\"android:textColor\">?attr/lv_item_desc_color</item>\n        <item name=\"android:layout_gravity\">center</item>\n        <item name=\"android:gravity\">center_vertical</item>\n    </style>\n\n    <style name=\"SettingItemSummary\">\n        <!--<iuserNamename=\"android:alpha\">0.80</item>-->\n        <item name=\"android:minWidth\">10dp</item>\n        <item name=\"android:layout_width\">wrap_content</item>\n        <item name=\"android:layout_height\">match_parent</item>\n        <item name=\"android:textSize\">12sp</item>\n        <item name=\"android:textColor\">?attr/lv_item_desc_color</item>\n        <item name=\"android:layout_gravity\">center</item>\n        <item name=\"android:gravity\">center|right</item>\n        <item name=\"android:drawableRight\">@drawable/ic_arrow_right</item>\n    </style>\n\n    <style name=\"string_grey\">\n        <item name=\"android:alpha\">0.36</item>\n        <item name=\"android:textColor\">@color/main_grey_dark</item>\n    </style>\n\n    <style name=\"bottom_bar_icon\">\n        <item name=\"android:layout_width\">@dimen/bottom_bar_height</item>\n        <item name=\"android:layout_height\">match_parent</item>\n        <item name=\"android:paddingTop\">9dp</item>\n        <item name=\"android:paddingBottom\">9dp</item>\n        <item name=\"android:layout_marginStart\">9dp</item>\n        <item name=\"android:layout_marginEnd\">9dp</item>\n    </style>\n\n    <style name=\"bottom_bar_iconfont\">\n        <item name=\"android:layout_width\">@dimen/bottom_bar_height</item>\n        <item name=\"android:layout_height\">match_parent</item>\n        <item name=\"android:gravity\">center</item>\n        <item name=\"android:padding\">6dp</item>\n        <item name=\"android:textSize\">20sp</item>\n        <item name=\"android:layout_marginStart\">8dp</item>\n        <item name=\"android:layout_marginEnd\">8dp</item>\n        <item name=\"android:textColor\">?attr/bottombar_fg</item>\n    </style>\n\n    <style name=\"top_bar_iconfont\">\n        <item name=\"android:layout_width\">@dimen/bottom_bar_height</item>\n        <item name=\"android:layout_height\">match_parent</item>\n        <item name=\"android:gravity\">center</item>\n        <item name=\"android:padding\">6dp</item>\n        <item name=\"android:textSize\">20sp</item>\n        <item name=\"android:layout_marginStart\">8dp</item>\n        <item name=\"android:layout_marginEnd\">8dp</item>\n        <item name=\"android:textColor\">?attr/topbar_fg</item>\n    </style>\n\n    <style name=\"AppTheme.Day.AppBarOverlay\" parent=\"ThemeOverlay.AppCompat.Dark.ActionBar\" />\n\n    <style name=\"AppTheme.Day.PopupOverlay\" parent=\"ThemeOverlay.AppCompat.Light\" />\n\n    <style name=\"FullscreenTheme\" parent=\"AppTheme.Day\">\n        <item name=\"android:actionBarStyle\">@style/FullscreenActionBarStyle</item>\n        <item name=\"android:windowActionBarOverlay\">true</item>\n        <item name=\"metaButtonBarStyle\">?android:attr/buttonBarStyle</item>\n        <item name=\"metaButtonBarButtonStyle\">?android:attr/buttonBarButtonStyle</item>\n\n        <item name=\"android:windowBackground\">@android:color/transparent</item>\n        <item name=\"android:windowIsTranslucent\">true</item>\n    </style>\n\n    <style name=\"FullscreenActionBarStyle\" parent=\"Widget.AppCompat.ActionBar\">\n        <item name=\"android:background\">@color/black_overlay</item>\n    </style>\n\n    <style name=\"OverflowMenuStyle\" parent=\"Widget.AppCompat.Light.PopupMenu.Overflow\">\n        <item name=\"overlapAnchor\">false</item>  <!--设置不覆盖锚点-->\n    </style>\n\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh-rCN/strings.xml",
    "content": "<resources>\n    <string name=\"app_id\">Loread</string>\n    <string name=\"app_name\">知微</string>\n    <string name=\"settings\">设置</string>\n    <string name=\"un_category\">未分类</string>\n    <string name=\"category\">分类</string>\n    <string name=\"tag\">Tag</string>\n    <string name=\"edit_category\">修改分类</string>\n    <string name=\"select_category\">选择分类</string>\n    <string name=\"approaching_the_world\">走近世界</string>\n<!--    <string-array name=\"markArticleListItem\">-->\n<!--        <item>向上标记为已读</item>-->\n<!--        <item>向下标记为已读</item>-->\n<!--        <item>保持未读</item>-->\n<!--    </string-array>-->\n    <string name=\"custom\">自定义</string>\n\n    <string name=\"select_tag\">选择标签</string>\n    <string name=\"select_user_agent\">选择 UserAgent 标识</string>\n    <string name=\"custom_user_agent\">自定义 UserAgent 标识</string>\n    <string name=\"enter_user_agent\">输入 UserAgent 标识</string>\n    <string name=\"remove_custom_user_agent\">删除自定义UA</string>\n    <string name=\"select_display_mode\">文章默认展示</string>\n\n    <string name=\"theme_switched_automatically\">已自动切换主题</string>\n    <string name=\"feed_url_display\">订阅网址：%s</string>\n    <string name=\"site_url_display\">站点网址：%s</string>\n    <string name=\"site_remark\">站点备注</string>\n    <string name=\"remark\">备注</string>\n    <string name=\"save_directory\">文章保存目录</string>\n    <string name=\"default_directory\">默认目录</string>\n    <string name=\"root_directory\">根目录</string>\n    <string name=\"edit_directory\">修改目录</string>\n    <string name=\"new_directory\">新增目录</string>\n    <string name=\"feed_title_as_directory\">订阅源的标题作为目录</string>\n    <string name=\"category_title_as_directory\">分类的标题作为目录</string>\n    <string name=\"custom_save_directory\">自定义保存目录</string>\n\n    <string name=\"saved_to_root_directory\">已保存至根目录</string>\n    <string name=\"saved_to_directory\">已保存至『%1$s』目录</string>\n\n\n    <string name=\"star_marked\">已星标</string>\n    <string name=\"star_marked_to_favorites\">已星标至『%1$s』收藏夹</string>\n\n    <string name=\"new_favorites\">新增收藏夹</string>\n    <string name=\"add_to_favorites\">加入收藏夹</string>\n    <string name=\"edit_favorites\">修改收藏夹</string>\n\n    <string name=\"etc\">等等</string>\n\n    <string name=\"default_x\">默认</string>\n    <string name=\"music\">音乐</string>\n    <string name=\"speak\">朗读</string>\n    <string name=\"auth_username\">用户名</string>\n    <string name=\"auth_password\">密码</string>\n    <string name=\"login\">登录</string>\n    <string name=\"please_input_account_id\">请输入用户名</string>\n    <string name=\"please_input_account_pw\">请输入密码</string>\n    <string name=\"no_account_register_now\">没有账户？现在注册</string>\n    <string name=\"do_you_want_to_download_files\">是否下载文件？</string>\n\n    <string name=\"plz_grant_permission_tips\">请授予权限，否则 APP 可能无法正常运行！</string>\n    <string name=\"authing\">授权中……</string>\n    <string name=\"fetch_user_info\">获取基本资料中……</string>\n    <string name=\"auth_failure_please_try_again\">授权失败，请重新尝试</string>\n    <string name=\"login_please_wait\">登录中，请稍等……</string>\n    <string name=\"login_failed_reason\">登录失败，原因：%1$s</string>\n    <string name=\"login_failure_please_try_again\">登录失败，请重新尝试</string>\n    <string name=\"login_failure_please_check_account\">登录失败，请检查账户与密码是否正确</string>\n    <string name=\"temporarily_not_supported\">暂时不支持</string>\n    \n    \n    <!-- Strings related to Settings -->\n    <string name=\"main_toolbar_title_search\">搜索：</string>\n\n    <string name=\"search_result_summary\">简介：%1$s | 订阅网址：%2$s</string>\n    <string name=\"search_result_subs\">%1$d 人订阅 | 周更新 %2$d 篇</string>\n\n    <plurals name=\"search_result_followers\">\n        <item quantity=\"other\">%1$d 人订阅</item>\n    </plurals>\n    <string name=\"search_result_articles\"> | 周更 %1$f 篇</string>\n\n    <string name=\"search_result_last_update_time\">上次更新 %1$s</string>\n\n    <string name=\"do_you_want_to_jump_the_application\">是否跳转到「 %1$s 」应用？</string>\n    <string name=\"corresponding\">相应的</string>\n    <string name=\"unable_to_find_app\">没有找到对应的应用</string>\n\n\n    <!--下面这个还没有用到-->\n    <string name=\"sync_article_content\">1 - 同步文章内容[%1$d / %2$d]</string>\n    <string name=\"sync_article_refs\">2 - 同步文章信息</string>\n    <string name=\"sync_feed_info\">3 - 同步订阅源信息</string>\n    <string name=\"fetch_article_full_content\">抓取文章全文</string>\n\n    <plurals name=\"articles_count\">\n        <item quantity=\"other\">%1$d 篇文章</item>\n    </plurals>\n    <plurals name=\"has_new_articles\">\n        <item quantity=\"other\">有 %1$d 篇新文章</item>\n    </plurals>\n\n<!--    <string name=\"has_new_articles\">有新文章</string>-->\n\n    <string name=\"share\">分享</string>\n    <string name=\"share_to\">分享到</string>\n    <string name=\"warning\">警告</string>\n    <string name=\"agree\">同意</string>\n    <string name=\"disagree\">反对</string>\n    <string name=\"cancel\">取消</string>\n    <string name=\"confirm\">确认</string>\n    <string name=\"ua_sign\">UA标识</string>\n\n    <string name=\"edit_success\">修改成功</string>\n    <string name=\"edit_fail\">修改失败</string>\n\n    <string name=\"mask_success\">标记成功</string>\n    <string name=\"mask_fail\">标记失败</string>\n\n    <string name=\"editing\">正在修改</string>\n    <string name=\"main_dialog_esc_positive\">好滴 ^_^</string>\n    <string name=\"main_dialog_esc_negative\">不！</string>\n\n    <string name=\"are_you_sure_that_unsubscribe_this_feed_link\">确定不再订阅该站点吗？</string>\n    <string name=\"main_dialog_confirm_mark_article_list\">确定要将以下文章标记为已读？</string>\n    <string name=\"main_dialog_esc_confirm\">确定退出app？</string>\n    <string name=\"setting_about_dialog_title\">感慨</string>\n    <string name=\"setting_about_dialog_content\">路很长，纵然远望，却不知方向。\\n抽支烟，思绪无常，奔跑着彷徨。\\n逃不脱的苟且，到不了的远方…\\n作于2016.10.07</string>\n\n\n    <string name=\"article_choose_tag_dialog_title\">选择标签</string>\n\n\n    <!--数据同步-->\n    <string name=\"sync\">同步</string>\n    <string name=\"setting_auto_sync_title\">自动同步</string>\n    <string name=\"setting_auto_sync_on_wifi_title\">仅在Wifi模式下自动同步</string>\n    <string name=\"setting_sync_frequency_title\">同步的时间间隔</string>\n    <string name=\"setting_down_img_title\">省流量下载图片</string>\n    <string name=\"proxy\">代理</string>\n    <!--界面显示-->\n    <string name=\"setting_auto_toggle_theme_title\">自动切换夜间主题</string>\n\n\n    <string name=\"other\">其他</string>\n    <!--操作-->\n    <string name=\"open_link_by_system_browser\">使用系统浏览器打开链接</string>\n    <!--缓存-->\n    <string name=\"setting_clear_day_title\">已读文章保留期限</string>\n    <string name=\"clear_article\">清理文章</string>\n    <string name=\"handle_crawldate\">优化爬取时间</string>\n\n    <!--备份-->\n    <string name=\"backup_now\">立即备份</string>\n    <string name=\"restore_backup\">恢复备份</string>\n    <string name=\"handle_saved_articles\">整理已保存的文章</string>\n\n    <string name=\"loading\">加载中</string>\n    <string name=\"upgrading\">升级中</string>\n    <string name=\"upgrading_please_wait_a_moment\">正在升级，请稍等…</string>\n    <string name=\"https_scheme\">https://</string>\n    <string name=\"inoreader_url\">https://inoreader.com</string>\n\n    <!--关于-->\n    <string name=\"about\">关于</string>\n    <string name=\"license\">许可</string>\n    <string name=\"feedback\">反馈问题</string>\n    <string name=\"qq_group\">Q群</string>\n    <string name=\"qq_group_number\">106211435</string>\n    <string name=\"esc\">退出</string>\n\n    <string name=\"add_account\">添加账号</string>\n    <string name=\"esc_account\">退出登录</string>\n    <string name=\"switch_account\">切换账号</string>\n\n    <string name=\"article_info\">本文相关信息</string>\n\n\n    <string name=\"config_the_feed\">配置该源</string>\n    <string name=\"theme_setting\">主题设置</string>\n\n    <string name=\"tips_no_net\">网络没有连接上，请检查一下</string>\n    <!--<string name=\"tag_choose_tips\">想看哪个分类？</string>-->\n\n    <string name=\"xx_minute\">%1$s 分钟</string>\n    <string name=\"xx_hour\">%1$s 小时</string>\n    <string name=\"clear_day_summary\">%1$s 天</string>\n\n    <string name=\"save_img\">保存图片</string>\n    <string name=\"share_img\">分享图片</string>\n    <string name=\"rename\">重命名</string>\n\n    <string name=\"mark_up\">向上标记已读</string>\n    <string name=\"mark_down\">向下标记已读</string>\n    <string name=\"mark_unread\">标记为未读</string>\n    <string name=\"speak_article\">播报该文章</string>\n\n    <string name=\"save_readability_content\">保存 Readability 内容？</string>\n    <string name=\"get_readability_ing\">正在获取</string>\n    <string name=\"get_readability_success\">获取成功</string>\n    <string name=\"get_readability_failure\">获取失败</string>\n    <string name=\"cancel_readability\">已取消展示获取的全文</string>\n\n    <string name=\"music_time\">0:00</string>\n    <string name=\"music_speed\">1.0</string>\n\n    <string name=\"edit_name\">编辑名称</string>\n    <string name=\"search\">搜索</string>\n    <string name=\"search_key\">输入关键词</string>\n    <string name=\"bag_network\">网络不好，请重试一下</string>\n    <string name=\"fail_try\">失败，请重试一下</string>\n\n    <string name=\"search_cloudy_feeds_result_count\">已搜到 %1$d 个订阅源</string>\n    <string name=\"search_header_search_cloudy_feeds\">搜索订阅源</string>\n    <string name=\"search_header_search_local_articles\">搜索手机上的文章</string>\n\n    <string name=\"share_link\">分享链接</string>\n    <string name=\"copy_link\">复制链接</string>\n    <string name=\"open_mode\">打开方式</string>\n\n    <string name=\"copy_success\">复制成功</string>\n\n    <string name=\"please_input_keyword\">请输入要搜索的词</string>\n\n    <string name=\"subscribe\">订阅</string>\n    <string name=\"subscribe_success\">订阅成功，请刷新同步</string>\n    <string name=\"subscribe_fail\">订阅失败，请重试</string>\n    <string name=\"unsubscribe_succeeded\">退订成功</string>\n    <string name=\"unsubscribe_failed\">退订失败: %1$s</string>\n\n    <string name=\"rename_failed\">重命名失败，请重试</string>\n\n    <string name=\"no_title\">无题</string>\n    <string name=\"no_thing\">无</string>\n\n    <string name=\"more_setting\">更多设置</string>\n    <string name=\"auto_mark_when_scrolling\">滚动时自动标记为已读</string>\n    <string name=\"down_img_on_wifi\">仅在连接Wifi时下载图片</string>\n    <string name=\"article_list_state\">文章筛选</string>\n    <string name=\"include_valueless\">包含无价值文章</string>\n    <string name=\"night_theme\">夜间主题</string>\n\n    <string name=\"article_list_order_asc\">上旧下新</string>\n    <string name=\"article_list_order_desc\">上新下旧</string>\n\n    <string name=\"all\">所有</string>\n    <string name=\"unread\">未读</string>\n    <string name=\"starred\">收藏</string>\n\n    <string name=\"rss\">RSS</string>\n    <string name=\"readability\">易读</string>\n    <string name=\"original\">原文</string>\n\n    <string name=\"view\">查看</string>\n    <string name=\"open_original_article\">查看原文</string>\n\n    <string name=\"refresh\">刷新</string>\n    <string name=\"stop\">暂停</string>\n    <string name=\"open_by_browser\">用浏览器打开</string>\n    <string name=\"download_file_position\">下载到目录：Download</string>\n    <string name=\"file_size\">文件大小：%1$s</string>\n    <string name=\"unknown\">未知</string>\n\n    <string name=\"wrong_username_or_password\">用户名或密码错误</string>\n\n    <string name=\"wrong_unknown\">未知错误</string>\n\n    <string name=\"is_filed_cannot_edit\">该文章已经被导出了，无法修改</string>\n\n    <string name=\"font_unstar\">&#xe64c;</string>\n    <string name=\"font_stared\">&#xe64b;</string>\n    <string name=\"font_readed\">&#xe72f;</string>\n    <string name=\"font_unread\">&#xe656;</string>\n\n    <string name=\"font_setting\">&#xe61f;</string>\n    <string name=\"font_theme\">&#xe650;</string>\n\n    <string name=\"font_unsave\">&#xe604;</string>\n    <string name=\"font_saved\">&#xe605;</string>\n    <string name=\"font_main\">&#xe60c;</string>\n\n    <string name=\"font_add\">&#xe699;</string>\n    <string name=\"font_cross\">&#xe6cb;</string>\n    <string name=\"font_tick\">&#xe636;</string>\n\n\n    <string name=\"font_group\">&#xe615;</string>\n\n    <string name=\"font_tag\">&#xe7c4;</string>\n    <string name=\"font_label\">&#xe7c5;</string>\n\n    <string name=\"font_arrow_down\">&#xe6a6;</string>\n    <string name=\"font_arrow_right\">&#xe6a8;</string>\n    <string name=\"font_arrow_left\">&#xe84e;</string>\n\n    <string name=\"font_list_reading\">&#xe612;</string>\n    <string name=\"font_list_unread\">&#xe613;</string>\n\n\n    <string name=\"font_no_data\">&#xe608;</string>\n    <string name=\"font_search\">&#xe741;</string>\n\n\n    <string name=\"font_share\">&#xe616;</string>\n    <string name=\"font_more\">&#xe60f;</string>\n\n    <string name=\"font_archive\">&#xe680;</string>\n\n    <string name=\"font_translation\">&#xe6ba;</string>\n    <string name=\"font_chrome\">&#xe678;</string>\n    <string name=\"font_book\">&#xe65b;</string>\n    <string name=\"font_readability\">&#xe611;</string>\n    <string name=\"font_feed_config\">&#xe72b;</string>\n    <string name=\"font_update\">&#xe645;</string>\n\n\n    <!--准备删除-->\n    <string name=\"font_last\">&#xe65b;</string>\n    <string name=\"font_next\">&#xe6a7;</string>\n    <string name=\"font_all_article\">&#xe6a7;</string>\n    <string name=\"font_list_stared\">&#xe617;</string>\n    <string name=\"font_list_unstar\">&#xe614;</string>\n\n    <!--气泡-->\n    <string name=\"font_untag_article\">&#xe84d;</string>\n    <!--<struserNamename=\"font_open_in_window\">&#xe610;</string>-->\n\n    <string name=\"title_activity_scrolling\">ScrollingActivity</string>\n    <string name=\"action_settings\">设置</string>\n    <string name=\"server_host\">服务器网址</string>\n    <string name=\"account\">账号</string>\n    <string name=\"password\">密码</string>\n    <string name=\"welcome\">\"欢迎！\"</string>\n    <string name=\"invalid_username\">用户名无效</string>\n    <string name=\"invalid_host\">Host 无效，格式为 https://example.com</string>\n    <string name=\"empty_host\">Host 为空，请重新设置</string>\n\n    <string name=\"invalid_password\">密码必须大于5个字符</string>\n\n    <string name=\"article_title_is\">文章标题为：</string>\n\n    <string name=\"summary_pre\">[此处有代码]</string>\n    <string name=\"summary_table\">[此处有表格]</string>\n    <string name=\"summary_link\">[此处有链接]</string>\n    <string name=\"summary_frame\">[此处有框架]</string>\n    <string name=\"summary_audio\">[此处有音频]</string>\n    <string name=\"summary_video\">[此处有视频]</string>\n    <string name=\"summary_image\">[此处有图片]</string>\n\n    <string name=\"frame\">[框架]</string>\n    <string name=\"audio\">[音频]</string>\n    <string name=\"video\">[视频]</string>\n    <string name=\"image\">[图片]</string>\n\n    <string name=\"inoreader_oauth\">InoReader (OAuth)</string>\n    <string name=\"inoreader_login\">InoReader (登录)</string>\n    <string name=\"feedly\">Feedly</string>\n    <string name=\"tinytinyrss\">TinyTinyRSS</string>\n    <string name=\"intro\">简介</string>\n\n    <string name=\"everything_is_rssible\">万物皆可 RSS</string>\n    <string name=\"developed_by_wizos\">行亘 · 作</string>\n\n    <string name=\"do_you_want_to_delete_data_of_this_account_after_logout\">退出登录后，是否删除该账号的所有数据？</string>\n    <string name=\"loread_plugin_must_be_installed\">注：须提前在 plugins.local 目录中安装 Loread 插件</string>\n    <string name=\"the_server_host_like\">服务器网址格式为“https://example.com”</string>\n\n    <string name=\"rss_summary\">\n        通俗得说，RSS 是一种订阅资讯的方式。\\n\\n就像在微博上关注张三、在B站上关注李四，你可以在『RSS 阅读器』中添加对应的订阅网址来关注他们。\\n\\n由此可实现订阅微博、知乎、优酷、公众号、博客、论坛……</string>\n    <string name=\"authorizing\">授权中……</string>\n\n    <string name=\"open_with\">选择打开方式</string>\n\n    <string name=\"feed_desc\">简介：</string>\n    <string name=\"site_url\">站点网址：</string>\n    <string name=\"feed_url\">订阅网址：</string>\n    <string name=\"unsubscribe\">退订</string>\n\n    <string name=\"display_filter_in_development\">展示筛选器，功能开发中</string>\n    \n    <string name=\"click_to_load_this_picture\">点击加载此图片</string>\n    <string name=\"loading_failed_click_here_to_retry\">加载失败，点击重试</string>\n    <string name=\"picture_error_click_here_to_retry\">图片异常，点击重试</string>\n\n    <string name=\"copied\">已复制</string>\n    <string name=\"the_rule_of_full_text_extraction_has_expired\">全文提取规则已失效：%1$s</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/xml/account_authenticator.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--android:accountType表示的是您的Account的类型，它必须是唯一的-->\n<account-authenticator\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:accountType=\"me.wizos.loreadx\"\n    android:icon=\"@mipmap/ic_launcher\"\n    android:label=\"@string/app_name\"\n    android:smallIcon=\"@mipmap/ic_launcher\"\n    />\n\n<!--如果您希望在已添加的帐户列表中点击我们的APP后出现一个自定义的页面，那么就加入accountPreferences属性-->"
  },
  {
    "path": "app/src/main/res/xml/account_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <PreferenceCategory\n        android:title=\"PreferenceCategory_title\" />\n    <PreferenceScreen\n        android:key=\"key1\"\n        android:title=\"PreferenceScreen_title\"\n        android:summary=\"PreferenceScreen_summary\">\n        <intent\n            android:action=\"key1.ACTION\"\n            android:targetPackage=\"key1.package\"\n            android:targetClass=\"key1.class\" />\n    </PreferenceScreen>\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/account_sync_adapter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<sync-adapter\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:accountType=\"me.wizos.loreadx\"\n    android:allowParallelSyncs=\"false\"\n    android:contentAuthority=\"project.test.com.myapplication.account.provide\"\n    android:isAlwaysSyncable=\"true\"\n    android:supportsUploading=\"false\"\n    android:userVisible=\"true\"/>\n\n<!--android:accountType表示的是您的Account的类型，一定要跟前面保持一致-->\n<!--android:allowParallelSyncs 是否支持多账号同时同步-->\n<!--android:contentAuthority 指定要同步的ContentProvider-->\n<!--android:isAlwaysSyncable 设置所有账号的isSyncable为1-->\n<!--android:supportsUploading 设置是否必须notifyChange通知才能同步-->\n<!--android:userVisible 设置是否在“设置”中显示-->"
  },
  {
    "path": "app/src/main/res/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config>\n    <base-config cleartextTrafficPermitted=\"true\">\n        <trust-anchors>\n            <certificates src=\"system\" />\n        </trust-anchors>\n    </base-config>\n</network-security-config>"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\n\nbuildscript {\n    repositories {\n        maven { url \"https://jitpack.io\" }\n        maven { url 'https://maven.google.com/' }\n        google()\n        jcenter()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:4.0.1'\n        classpath fileTree(dir: 'plugin', include: ['*.jar'])\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        maven {url 'https://maven.google.com/'}\n        jcenter()\n        maven { url \"https://jitpack.io\" }\n//        maven { url 'https://maven.aliyun.com/repository/google'}\n//        maven { url 'https://maven.aliyun.com/repository/jcenter'}\n//        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }\n//        maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' }\n//        maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' }\n//        maven { url 'http://maven.aliyun.com/nexus/content/repositories/gradle-plugin' }\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "config.json",
    "content": "{\r\n  \"app\": {\r\n    \"version\": 13,\r\n    \"versionName\": \"2.3\",\r\n    \"url\": \"http://wizos.me/test.apk\",\r\n    \"content\": \"测试升级\"\r\n  }\r\n}"
  },
  {
    "path": "floatwindow/.gitignore",
    "content": "/build\n/src/androidTest/\n/src/test/\n"
  },
  {
    "path": "floatwindow/build.gradle",
    "content": "apply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion 28\n    buildToolsVersion \"28.0.3\"\n\n\n    defaultConfig {\n        minSdkVersion 22\n        targetSdkVersion 28\n        versionCode 1\n        versionName \"1.0\"\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    androidTestImplementation('androidx.test.espresso:espresso-core:3.2.0', {\n        exclude group: 'com.android.support', module: 'support-annotations'\n    })\n    implementation 'androidx.appcompat:appcompat:1.1.0'\n    testImplementation 'junit:junit:4.13'\n}\n\n"
  },
  {
    "path": "floatwindow/proguard-rules.pro",
    "content": "# Add project specific ProGuard replaceUrl here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "floatwindow/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          package=\"com.example.fixedfloatwindow\">\n<application>\n<activity\n    android:name=\"com.yhao.floatwindow.permission.FloatActivity\"\n    android:configChanges=\"keyboardHidden|orientation|screenSize\"\n    android:launchMode=\"standard\"\n    android:theme=\"@style/permission_PermissionActivity\"\n    android:windowSoftInputMode=\"stateHidden|stateAlwaysHidden\"/>\n</application>\n</manifest>"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/adaptation/Miui.java",
    "content": "package com.yhao.floatwindow.adaptation;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.provider.Settings;\nimport android.view.View;\nimport android.view.WindowManager;\n\nimport com.yhao.floatwindow.base.FloatLifecycle;\nimport com.yhao.floatwindow.interfaces.PermissionListener;\nimport com.yhao.floatwindow.interfaces.ResumedListener;\nimport com.yhao.floatwindow.util.LogUtil;\nimport com.yhao.floatwindow.util.PermissionUtil;\n\nimport java.lang.reflect.Field;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static com.yhao.floatwindow.adaptation.Rom.isIntentAvailable;\n\n/**\n * Created by yhao on 2017/12/30.\n * https://github.com/yhaolpz\n * <p>\n * 需要清楚：一个MIUI版本对应小米各种机型，基于不同的安卓版本，但是权限设置页跟MIUI版本有关\n * 测试TYPE_TOAST类型：\n * 7.0：\n * 小米      5        MIUI8         -------------------- 失败\n * 小米   Note2       MIUI9         -------------------- 失败\n * 6.0.1\n * 小米   5                         -------------------- 失败\n * 小米   红米note3                  -------------------- 失败\n * 6.0：\n * 小米   5                         -------------------- 成功\n * 小米   红米4A      MIUI8         -------------------- 成功\n * 小米   红米Pro     MIUI7         -------------------- 成功\n * 小米   红米Note4   MIUI8         -------------------- 失败\n * <p>\n * 经过各种横向纵向测试对比，得出一个结论，就是小米对TYPE_TOAST的处理机制毫无规律可言！\n * 跟Android版本无关，跟MIUI版本无关，addView方法也不报错\n * 所以最后对小米6.0以上的适配方法是：不使用 TYPE_TOAST 类型，统一申请权限\n */\n\npublic class Miui {\n\n    private static final String miui = \"ro.miui.ui.version.name\";\n    private static final String miui5 = \"V5\";\n    private static final String miui6 = \"V6\";\n    private static final String miui7 = \"V7\";\n    private static final String miui8 = \"V8\";\n    private static final String miui9 = \"V9\";\n    private static List<PermissionListener> mPermissionListenerList;\n    private static PermissionListener mPermissionListener;\n\n\n    public static boolean rom() {\n        LogUtil.d(\" Miui  : \" + Miui.getProp());\n        return Build.MANUFACTURER.equals(\"Xiaomi\");\n    }\n\n    private static String getProp() {\n        return Rom.getProp(miui);\n    }\n\n    /**\n     * Android6.0以下申请权限\n     */\n    public static void req(final Context context, PermissionListener permissionListener) {\n        if (PermissionUtil.hasPermission(context)) {\n            permissionListener.onSuccess();\n            return;\n        }\n        if (mPermissionListenerList == null) {\n            mPermissionListenerList = new ArrayList<>();\n            mPermissionListener = new PermissionListener() {\n                @Override\n                public void onSuccess() {\n                    for (PermissionListener listener : mPermissionListenerList) {\n                        listener.onSuccess();\n                    }\n                    mPermissionListenerList.clear();\n                }\n                @Override\n                public void onFail() {\n                    for (PermissionListener listener : mPermissionListenerList) {\n                        listener.onFail();\n                    }\n                    mPermissionListenerList.clear();\n                }\n            };\n            req_(context);\n        }\n        mPermissionListenerList.add(permissionListener);\n    }\n\n\n    private static void req_(final Context context) {\n        switch (getProp()) {\n            case miui5:\n                reqForMiui5(context);\n                break;\n            case miui6:\n            case miui7:\n                reqForMiui67(context);\n                break;\n            case miui8:\n            case miui9:\n                reqForMiui89(context);\n                break;\n        }\n        FloatLifecycle.setResumedListener(new ResumedListener() {\n            @Override\n            public void onResumed() {\n                if (PermissionUtil.hasPermission(context)) {\n                    mPermissionListener.onSuccess();\n                } else {\n                    mPermissionListener.onFail();\n                }\n            }\n        });\n    }\n\n\n    private static void reqForMiui5(Context context) {\n        String packageName = context.getPackageName();\n        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);\n        Uri uri = Uri.fromParts(\"package\", packageName, null);\n        intent.setData(uri);\n        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n        if (isIntentAvailable(intent, context)) {\n            context.startActivity(intent);\n        } else {\n            LogUtil.e(\"intent is not available!\");\n        }\n    }\n\n    private static void reqForMiui67(Context context) {\n        Intent intent = new Intent(\"miui.intent.action.APP_PERM_EDITOR\");\n        intent.setClassName(\"com.miui.securitycenter\",\n                \"com.miui.permcenter.permissions.AppPermissionsEditorActivity\");\n        intent.putExtra(\"extra_pkgname\", context.getPackageName());\n        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n        if (isIntentAvailable(intent, context)) {\n            context.startActivity(intent);\n        } else {\n            LogUtil.e(\"intent is not available!\");\n        }\n    }\n\n    private static void reqForMiui89(Context context) {\n        Intent intent = new Intent(\"miui.intent.action.APP_PERM_EDITOR\");\n        intent.setClassName(\"com.miui.securitycenter\", \"com.miui.permcenter.permissions.PermissionsEditorActivity\");\n        intent.putExtra(\"extra_pkgname\", context.getPackageName());\n        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n        if (isIntentAvailable(intent, context)) {\n            context.startActivity(intent);\n        } else {\n            intent = new Intent(\"miui.intent.action.APP_PERM_EDITOR\");\n            intent.setPackage(\"com.miui.securitycenter\");\n            intent.putExtra(\"extra_pkgname\", context.getPackageName());\n            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n            if (isIntentAvailable(intent, context)) {\n                context.startActivity(intent);\n            } else {\n                LogUtil.e(\"intent is not available!\");\n            }\n        }\n    }\n\n\n    /**\n     * 有些机型在添加TYPE-TOAST类型时会自动改为TYPE_SYSTEM_ALERT，通过此方法可以屏蔽修改\n     * 但是...即使成功显示出悬浮窗，移动的话也会崩溃\n     */\n    private static void addViewToWindow(WindowManager wm, View view, WindowManager.LayoutParams params) {\n        setMiUI_International(true);\n        wm.addView(view, params);\n        setMiUI_International(false);\n    }\n\n\n    private static void setMiUI_International(boolean flag) {\n        try {\n            Class BuildForMi = Class.forName(\"miui.os.Build\");\n            Field isInternational = BuildForMi.getDeclaredField(\"IS_INTERNATIONAL_BUILD\");\n            isInternational.setAccessible(true);\n            isInternational.setBoolean(null, flag);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/adaptation/Rom.java",
    "content": "package com.yhao.floatwindow.adaptation;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.pm.PackageManager;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\n\n/**\n * Created by yhao on 2017/12/30.\n * https://github.com/yhaolpz\n */\n\nclass Rom {\n\n    static boolean isIntentAvailable(Intent intent, Context context) {\n        return intent != null && context.getPackageManager().queryIntentActivities(\n                intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0;\n    }\n\n\n    static String getProp(String name) {\n        BufferedReader input = null;\n        try {\n            Process p = Runtime.getRuntime().exec(\"getprop \" + name);\n            input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024);\n            String line = input.readLine();\n            input.close();\n            return line;\n        } catch (IOException ex) {\n            return null;\n        } finally {\n            if (input != null) {\n                try {\n                    input.close();\n                } catch (IOException e) {\n                    e.printStackTrace();\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/base/FloatLifecycle.java",
    "content": "package com.yhao.floatwindow.base;\n\nimport android.app.Activity;\nimport android.app.Application;\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.view.Surface;\nimport android.view.WindowManager;\n\nimport com.yhao.floatwindow.interfaces.LifecycleListener;\nimport com.yhao.floatwindow.interfaces.ResumedListener;\nimport com.yhao.floatwindow.util.ActivityCounter;\n\n/**\n *\n * @author yhao\n * @date 17-12-1\n * 用于控制悬浮窗显示周期\n * 使用了三种方法针对返回桌面时隐藏悬浮按钮\n * 1.startCount计数，针对back到桌面可以及时隐藏\n * 2.resumeCount计时，针对一些只执行onPause不执行onStop的奇葩情况\n * 3.监听home键，从而及时隐藏\n */\n\npublic class FloatLifecycle extends BroadcastReceiver implements Application.ActivityLifecycleCallbacks {\n\n    private static final String SYSTEM_DIALOG_REASON_KEY = \"reason\";\n    private static final String SYSTEM_DIALOG_REASON_HOME_KEY = \"homekey\";\n    private static final long delay = 300;\n    private Handler mHandler;\n    private Class[] activities;\n    private boolean showFlag;\n//    private int startCount;\n//    private int resumeCount;\n    private boolean appBackground;\n    private LifecycleListener mLifecycleListener;\n    private static ResumedListener sResumedListener;\n    private static int num = 0;\n    private WindowManager mWindowManager;\n\n    public FloatLifecycle(Context applicationContext, boolean showFlag, Class[] activities, LifecycleListener lifecycleListener) {\n        this.showFlag = showFlag;\n        this.activities = activities;\n        num++;\n        mLifecycleListener = lifecycleListener;\n        mHandler = new Handler();\n        // FIXME: 2019/6/2 使用 ActivityLifecycleCallbacks 的方案是有瑕疵的，导致 FloatWindow 必须要在 App 的 onCreate 中初始化。如果是在 Activity 中 初始化，会导致根据当前计数的resume/stop状态的Activity不准确。\n        ((Application) applicationContext).registerActivityLifecycleCallbacks(this);\n        mWindowManager = (WindowManager) applicationContext.getSystemService(Context.WINDOW_SERVICE);\n        IntentFilter configChangeFilter = new IntentFilter();\n        configChangeFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);\n        configChangeFilter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);\n        applicationContext.registerReceiver(this, configChangeFilter);\n    }\n\n    public static void setResumedListener(ResumedListener resumedListener) {\n        sResumedListener = resumedListener;\n    }\n\n    private boolean needShow(Activity activity) {\n        if (activities == null) {\n            return true;\n        }\n        for (Class a : activities) {\n            if (a.isInstance(activity)) {\n                return showFlag;\n            }\n        }\n        return !showFlag;\n    }\n\n\n    @Override\n    public void onReceive(Context context, Intent intent) {\n        String action = intent.getAction();\n        if (action != null && action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) {\n            String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);\n            if (SYSTEM_DIALOG_REASON_HOME_KEY.equals(reason)) {\n                mLifecycleListener.onBackToDesktop();\n            }\n        } else if (action != null && action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) {\n            switch (mWindowManager.getDefaultDisplay().getRotation()) {\n                case Surface.ROTATION_0:\n                case Surface.ROTATION_180:\n                    //竖屏\n                    mLifecycleListener.onPortrait();\n                    break;\n                case Surface.ROTATION_90:\n                case Surface.ROTATION_270:\n                    //横屏\n                    mLifecycleListener.onLandscape();\n                    break;\n                default:\n                    break;\n            }\n        }\n    }\n\n    @Override\n    public void onActivityResumed(Activity activity) {\n        if (sResumedListener != null) {\n            num--;\n            if (num == 0) {\n                sResumedListener.onResumed();\n                sResumedListener = null;\n            }\n        }\n//        resumeCount++;\n        if (needShow(activity)) {\n            mLifecycleListener.onShow();\n        } else {\n            mLifecycleListener.onHide();\n        }\n\n        if (appBackground) {\n            appBackground = false;\n        }\n    }\n\n    @Override\n    public void onActivityPaused(final Activity activity) {\n//        resumeCount--;\n        mHandler.postDelayed(new Runnable() {\n            @Override\n            public void run() {\n                if( ActivityCounter.isOnBackground() ){\n                    mLifecycleListener.onBackToDesktop();\n                }\n//                if (resumeCount == 0) {\n//                    appBackground = true;\n//                    LogUtil.e(\"接onActivityPaused：\" );\n//                    mLifecycleListener.onBackToDesktop();\n//                }\n            }\n        }, delay);\n\n    }\n\n    @Override\n    public void onActivityStarted(Activity activity) {\n//        startCount++;\n    }\n\n    @Override\n    public void onActivityStopped(Activity activity) {\n        if( ActivityCounter.isOnBackground() ){\n            mLifecycleListener.onBackToDesktop();\n        }\n//        startCount--;\n//        if (startCount == 0) {\n//            LogUtil.e(\"接onActivityStopped：\" );\n//            mLifecycleListener.onBackToDesktop();\n//        }\n    }\n\n    @Override\n    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {\n    }\n\n    @Override\n    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {\n\n    }\n\n    @Override\n    public void onActivityDestroyed(Activity activity) {\n\n    }\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/constant/MoveType.java",
    "content": "package com.yhao.floatwindow.constant;\n\nimport androidx.annotation.IntDef;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Created by yhao on 2017/12/22.\n * https://github.com/yhaolpz\n */\n\npublic class MoveType {\n    public static final int fixed = 0;\n    // 不可拖动\n    public static final int inactive = 1;\n    // 可拖动\n    public static final int active = 2;\n    // 可拖动，释放后自动贴边 （默认）\n    public static final int slide = 3;\n    // 可拖动，释放后自动回到原位置\n    public static final int back = 4;\n\n    @IntDef({fixed, inactive, active, slide, back})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface MOVE_TYPE {\n    }\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/constant/Screen.java",
    "content": "package com.yhao.floatwindow.constant;\n\nimport androidx.annotation.IntDef;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Created by yhao on 2017/12/23.\n * https://github.com/yhaolpz\n */\n\npublic class Screen {\n    public static final int width = 0;\n    public static final int height = 1;\n\n    @IntDef({width, height})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface screenType {\n    }\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/interfaces/FloatView.java",
    "content": "package com.yhao.floatwindow.interfaces;\n\nimport android.view.View;\n\n/**\n * Created by yhao on 17-11-14.\n * https://github.com/yhaolpz\n */\n\npublic interface FloatView {\n\n    void setSize(int width, int height);\n\n    void setView(View view);\n\n    void setGravity(int gravity, int xOffset, int yOffset);\n\n    void init();\n\n    void dismiss();\n\n    void updateXY(int x, int y);\n\n    void updateX(int x);\n\n    void updateY(int y);\n\n    int getX();\n\n    int getY();\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/interfaces/IFloatWindow.java",
    "content": "package com.yhao.floatwindow.interfaces;\n\nimport com.yhao.floatwindow.constant.Screen;\n\n/**\n * Created by yhao on 2017/12/22.\n * https://github.com/yhaolpz\n */\n\npublic interface IFloatWindow {\n    void showByUser();\n\n    void hideByUser();\n\n    boolean isShowing();\n\n    int getX();\n\n    int getY();\n\n    void updateX(int x);\n\n    void updateX(@Screen.screenType int screenType, float ratio);\n\n    void updateY(int y);\n\n    void updateY(@Screen.screenType int screenType, float ratio);\n\n    void dismiss();\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/interfaces/LifecycleListener.java",
    "content": "package com.yhao.floatwindow.interfaces;\n\n/**\n *\n * @author yhao\n * @date 2017/12/22\n * https://github.com/yhaolpz\n */\n\npublic interface LifecycleListener {\n\n    void onShow();\n\n    void onHide();\n\n    void onBackToDesktop();\n\n    void onPortrait();\n\n    void onLandscape();\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/interfaces/PermissionListener.java",
    "content": "package com.yhao.floatwindow.interfaces;\n\n/**\n * Created by yhao on 2017/11/14.\n * https://github.com/yhaolpz\n */\npublic interface PermissionListener {\n    // void onReady();\n    void onSuccess();\n    void onFail();\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/interfaces/ResumedListener.java",
    "content": "package com.yhao.floatwindow.interfaces;\n\n/**\n * Created by yhao on 2017/12/30.\n * https://github.com/yhaolpz\n */\n\npublic interface ResumedListener {\n    void onResumed();\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/interfaces/ViewStateListener.java",
    "content": "package com.yhao.floatwindow.interfaces;\n\n/**\n * Created by yhao on 2018/5/5\n * https://github.com/yhaolpz\n */\npublic interface ViewStateListener {\n    void onPositionUpdate(int x, int y);\n\n    void onShow();\n\n    void onHide();\n\n    void onShowByUser();\n\n    void onHideByUser();\n\n    void onDismiss();\n\n    void onMoveAnimStart();\n\n    void onMoveAnimEnd();\n\n    void onBackToDesktop();\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/permission/FloatActivity.java",
    "content": "package com.yhao.floatwindow.permission;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.provider.Settings;\nimport android.widget.Toast;\n\nimport androidx.annotation.RequiresApi;\n\nimport com.yhao.floatwindow.interfaces.PermissionListener;\nimport com.yhao.floatwindow.util.PermissionUtil;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 用于在内部自动申请权限\n * https://github.com/yhaolpz\n */\n\npublic class FloatActivity extends Activity {\n    private static List<PermissionListener> mPermissionListenerList;\n    private static PermissionListener mPermissionListener;\n\n    @RequiresApi(api = Build.VERSION_CODES.M)\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        requestAlertWindowPermission();\n    }\n\n    @RequiresApi(api = Build.VERSION_CODES.M)\n    private void requestAlertWindowPermission() {\n        Toast.makeText(this, \"请打开显示悬浮窗权限！\", Toast.LENGTH_LONG).show();\n        Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);\n        intent.setData(Uri.parse(\"package:\" + getPackageName()));\n        startActivityForResult(intent, 12);\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        // 756232212\n        if (requestCode == 12) {\n            if (PermissionUtil.hasPermissionOnActivityResult(this)) {\n                mPermissionListener.onSuccess();\n            } else {\n                mPermissionListener.onFail();\n            }\n        }\n        finish();\n    }\n\n    public static synchronized void request(Context context, PermissionListener permissionListener) {\n        if (PermissionUtil.hasPermission(context)) {\n            permissionListener.onSuccess();\n            return;\n        }\n        if (mPermissionListenerList == null) {\n            mPermissionListenerList = new ArrayList<>();\n            mPermissionListener = new PermissionListener() {\n                @Override\n                public void onSuccess() {\n                    for (PermissionListener listener : mPermissionListenerList) {\n                        listener.onSuccess();\n                    }\n                    mPermissionListenerList.clear();\n                }\n\n                @Override\n                public void onFail() {\n                    for (PermissionListener listener : mPermissionListenerList) {\n                        listener.onFail();\n                    }\n                    mPermissionListenerList.clear();\n                }\n            };\n            Intent intent = new Intent(context, FloatActivity.class);\n            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n            context.startActivity(intent);\n        }\n        mPermissionListenerList.add(permissionListener);\n    }\n\n\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/util/ActivityCounter.java",
    "content": "package com.yhao.floatwindow.util;\n\nimport android.app.Activity;\nimport android.app.Application;\nimport android.os.Bundle;\n\n/**\n * Created by Wizos on 2019/6/2.\n */\n\npublic class ActivityCounter implements Application.ActivityLifecycleCallbacks {\n    private static int startCount;\n    private static int resumeCount;\n\n    @Override\n    public void onActivityResumed(Activity activity) {\n    }\n\n    @Override\n    public void onActivityPaused(final Activity activity) {\n        resumeCount--;\n    }\n\n    @Override\n    public void onActivityStarted(Activity activity) {\n        startCount++;\n    }\n\n    @Override\n    public void onActivityStopped(Activity activity) {\n        startCount--;\n    }\n\n    @Override\n    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {\n    }\n\n    @Override\n    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {\n    }\n\n    @Override\n    public void onActivityDestroyed(Activity activity) {\n    }\n\n    public static boolean isOnBackground() {\n        return( resumeCount==0 || startCount == 0);\n    }\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/util/DensityUtil.java",
    "content": "package com.yhao.floatwindow.util;\n\nimport android.content.Context;\nimport android.graphics.Point;\nimport android.graphics.Rect;\nimport android.util.DisplayMetrics;\nimport android.view.LayoutInflater;\nimport android.view.View;\n\n/**\n * Created by yhao on 2017/12/22.\n * https://github.com/yhaolpz\n */\n\npublic class DensityUtil {\n\n\n    public static View inflate(Context applicationContext, int layoutId) {\n        LayoutInflater inflate = (LayoutInflater) applicationContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);\n        return inflate.inflate(layoutId, null);\n    }\n\n    private static Point sPoint;\n\n    public static int getScreenWidth(Context context) {\n//        if (sPoint == null) {\n//            sPoint = new Point();\n//            WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);\n//            wm.getDefaultDisplay().getSize(sPoint);\n//        }\n//        return sPoint.x;\n        DisplayMetrics dm = context.getResources().getDisplayMetrics();\n        return dm.widthPixels;\n    }\n\n    public static int getScreenHeight(Context context) {\n//        if (sPoint == null) {\n//            sPoint = new Point();\n//            WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);\n//            wm.getDefaultDisplay().getSize(sPoint);\n//        }\n//        return sPoint.y;\n        DisplayMetrics dm = context.getResources().getDisplayMetrics();\n        return dm.heightPixels;\n    }\n\n    public static int getStatusBarHeight(Context context){\n        int resourceId = context.getResources().getIdentifier(\"status_bar_height\", \"dimen\", \"android\");\n        if (resourceId > 0) {\n            return context.getResources().getDimensionPixelSize(resourceId);\n        }\n        return 0;\n    }\n\n    /**\n     * dp 转 px\n     *\n     * @param context context\n     * @param dpValue dpValue\n     * @return px\n     */\n    public static int dip2px(Context context, float dpValue) {\n        final float scale = context.getResources().getDisplayMetrics().density;\n        return (int) (dpValue * scale + 0.5f);\n    }\n\n    static boolean isViewVisible(View view) {\n        return view.getGlobalVisibleRect(new Rect());\n    }\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/util/LogUtil.java",
    "content": "package com.yhao.floatwindow.util;\n\nimport android.util.Log;\n\n\n/**\n * Created by yhao on 2017/12/29.\n * https://github.com/yhaolpz\n */\n\npublic class LogUtil {\n\n    private static final String TAG = \"FloatWindow\";\n\n    public static void e(String message) {\n        Log.e(TAG, message);\n    }\n\n    public static void d(String message) {\n        Log.d(TAG, message);\n    }\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/util/PermissionUtil.java",
    "content": "package com.yhao.floatwindow.util;\n\nimport android.app.AppOpsManager;\nimport android.content.Context;\nimport android.graphics.PixelFormat;\nimport android.os.Binder;\nimport android.os.Build;\nimport android.provider.Settings;\nimport android.view.View;\nimport android.view.WindowManager;\n\nimport androidx.annotation.RequiresApi;\n\nimport java.lang.reflect.Method;\n\n/**\n * Created by yhao on 2017/12/29.\n * https://github.com/yhaolpz\n */\n\npublic class PermissionUtil {\n\n    public static boolean hasPermission(Context context) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            return Settings.canDrawOverlays(context);\n        } else {\n            return hasPermissionBelowMarshmallow(context);\n        }\n    }\n\n    public static boolean hasPermissionOnActivityResult(Context context) {\n        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O) {\n            return hasPermissionForO(context);\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            return Settings.canDrawOverlays(context);\n        } else {\n            return hasPermissionBelowMarshmallow(context);\n        }\n    }\n\n    /**\n     * 6.0以下判断是否有权限\n     * 理论上6.0以上才需处理权限，但有的国内rom在6.0以下就添加了权限\n     * 其实此方式也可以用于判断6.0以上版本，只不过有更简单的canDrawOverlays代替\n     */\n    static boolean hasPermissionBelowMarshmallow(Context context) {\n        try {\n            AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);\n            Method dispatchMethod = AppOpsManager.class.getMethod(\"checkOp\", int.class, int.class, String.class);\n            //AppOpsManager.OP_SYSTEM_ALERT_WINDOW = 24\n            return AppOpsManager.MODE_ALLOWED == (Integer) dispatchMethod.invoke(\n                    manager, 24, Binder.getCallingUid(), context.getApplicationContext().getPackageName());\n        } catch (Exception e) {\n            return false;\n        }\n    }\n\n\n    /**\n     * 用于判断8.0时是否有权限，仅用于OnActivityResult\n     * 针对8.0官方bug:在用户授予权限后Settings.canDrawOverlays或checkOp方法判断仍然返回false\n     */\n    @RequiresApi(api = Build.VERSION_CODES.M)\n    private static boolean hasPermissionForO(Context context) {\n        try {\n            WindowManager mgr = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);\n            if (mgr == null) return false;\n            View viewToAdd = new View(context);\n            WindowManager.LayoutParams params = new WindowManager.LayoutParams(0, 0,\n                    android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ?\n                            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,\n                    WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,\n                    PixelFormat.TRANSPARENT);\n            viewToAdd.setLayoutParams(params);\n            mgr.addView(viewToAdd, params);\n            mgr.removeView(viewToAdd);\n            return true;\n        } catch (Exception e) {\n            LogUtil.e(\"hasPermissionForO e:\" + e.toString());\n        }\n        return false;\n    }\n\n\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/view/FloatPhone.java",
    "content": "package com.yhao.floatwindow.view;\n\nimport android.content.Context;\nimport android.graphics.PixelFormat;\nimport android.os.Build;\nimport android.view.View;\nimport android.view.WindowManager;\n\nimport com.yhao.floatwindow.adaptation.Miui;\nimport com.yhao.floatwindow.interfaces.FloatView;\nimport com.yhao.floatwindow.interfaces.PermissionListener;\nimport com.yhao.floatwindow.permission.FloatActivity;\n\n/**\n * Created by yhao on 17-11-14.\n * https://github.com/yhaolpz\n */\n\nclass FloatPhone implements FloatView {\n\n    private final Context mContext;\n\n    private final WindowManager mWindowManager;\n    private final WindowManager.LayoutParams mLayoutParams;\n    private View mView;\n    private int mX, mY;\n    private boolean isRemove = false;\n    private PermissionListener mPermissionListener;\n\n    FloatPhone(Context applicationContext, PermissionListener permissionListener, Boolean childViewTouchable) {\n        mContext = applicationContext;\n        mPermissionListener = permissionListener;\n        mWindowManager = (WindowManager) applicationContext.getSystemService(Context.WINDOW_SERVICE);\n        mLayoutParams = new WindowManager.LayoutParams();\n        mLayoutParams.format = PixelFormat.RGBA_8888;\n\n        mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;\n        if (!childViewTouchable) {\n            mLayoutParams.flags += WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;\n        }\n        mLayoutParams.windowAnimations = 0;\n    }\n\n    @Override\n    public void setSize(int width, int height) {\n        mLayoutParams.width = width;\n        mLayoutParams.height = height;\n    }\n\n    @Override\n    public void setView(View view) {\n        mView = view;\n    }\n\n    @Override\n    public void setGravity(int gravity, int xOffset, int yOffset) {\n        mLayoutParams.gravity = gravity;\n        mLayoutParams.x = mX = xOffset;\n        mLayoutParams.y = mY = yOffset;\n    }\n\n\n    @Override\n    public void init() {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {\n            req();\n        } else if (Miui.rom()) {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n                req();\n            } else {\n                mLayoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;\n                Miui.req(mContext, new PermissionListener() {\n                    @Override\n                    public void onSuccess() {\n                        mWindowManager.addView(mView, mLayoutParams);\n                        if (mPermissionListener != null) {\n                            mPermissionListener.onSuccess();\n                        }\n                    }\n\n                    @Override\n                    public void onFail() {\n                        if (mPermissionListener != null) {\n                            mPermissionListener.onFail();\n                        }\n                    }\n                });\n            }\n        } else {\n            try {\n                mLayoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;\n                mWindowManager.addView(mView, mLayoutParams);\n            } catch (Exception e) {\n                mWindowManager.removeView(mView);\n                req();\n            }\n        }\n    }\n\n    private void req() {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;\n        } else {\n            mLayoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;\n        }\n        FloatActivity.request(mContext, new PermissionListener() {\n            @Override\n            public void onSuccess() {\n                mWindowManager.addView(mView, mLayoutParams);\n                if (mPermissionListener != null) {\n                    mPermissionListener.onSuccess();\n                }\n            }\n\n            @Override\n            public void onFail() {\n                if (mPermissionListener != null) {\n                    mPermissionListener.onFail();\n                }\n            }\n        });\n    }\n\n    @Override\n    public void dismiss() {\n        isRemove = true;\n        mWindowManager.removeView(mView);\n    }\n\n    @Override\n    public void updateXY(int x, int y) {\n        if (isRemove) {\n            return;\n        }\n        mLayoutParams.x = mX = x;\n        mLayoutParams.y = mY = y;\n        mWindowManager.updateViewLayout(mView, mLayoutParams);\n    }\n\n    @Override\n    public void updateX(int x) {\n        if (isRemove) {\n            return;\n        }\n        mLayoutParams.x = mX = x;\n        mWindowManager.updateViewLayout(mView, mLayoutParams);\n    }\n\n    @Override\n    public void updateY(int y) {\n        if (isRemove) {\n            return;\n        }\n        mLayoutParams.y = mY = y;\n        mWindowManager.updateViewLayout(mView, mLayoutParams);\n    }\n\n    @Override\n    public int getX() {\n        return mX;\n    }\n\n    @Override\n    public int getY() {\n        return mY;\n    }\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/view/FloatToast.java",
    "content": "package com.yhao.floatwindow.view;\n\nimport android.content.Context;\nimport android.view.View;\nimport android.view.WindowManager;\nimport android.widget.Toast;\n\nimport com.yhao.floatwindow.interfaces.FloatView;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\n\n/**\n * 自定义 toast 方式，无需申请权限\n * 当前版本暂时用 TYPE_TOAST 代替，后续版本可能会再融入此方式\n */\n\nclass FloatToast implements FloatView {\n\n\n    private Toast toast;\n\n    private Object mTN;\n    private Method show;\n    private Method hide;\n\n    private int mWidth;\n    private int mHeight;\n    Boolean mTouchable;\n\n\n    FloatToast(Context applicationContext,Boolean childViewTouchable) {\n        toast = new Toast(applicationContext);\n        mTouchable = childViewTouchable;\n    }\n\n\n    @Override\n    public void setSize(int width, int height) {\n        mWidth = width;\n        mHeight = height;\n    }\n\n    @Override\n    public void setView(View view) {\n        toast.setView(view);\n        initTN(mTouchable);\n    }\n\n    @Override\n    public void setGravity(int gravity, int xOffset, int yOffset) {\n        toast.setGravity(gravity, xOffset, yOffset);\n    }\n\n    @Override\n    public void init() {\n        try {\n            show.invoke(mTN);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n\n    @Override\n    public void dismiss() {\n        try {\n            hide.invoke(mTN);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n\n    @Override\n    public void updateXY(int x, int y) {\n\n    }\n\n    @Override\n    public void updateX(int x) {\n\n    }\n\n    @Override\n    public void updateY(int y) {\n\n    }\n\n    @Override\n    public int getX() {\n        return 0;\n    }\n\n    @Override\n    public int getY() {\n        return 0;\n    }\n\n\n    private void initTN(Boolean childViewTouchable) {\n        try {\n            Field tnField = toast.getClass().getDeclaredField(\"mTN\");\n            tnField.setAccessible(true);\n            mTN = tnField.get(toast);\n            show = mTN.getClass().getMethod(\"show\");\n            hide = mTN.getClass().getMethod(\"hide\");\n            Field tnParamsField = mTN.getClass().getDeclaredField(\"mParams\");\n            tnParamsField.setAccessible(true);\n            WindowManager.LayoutParams params = (WindowManager.LayoutParams) tnParamsField.get(mTN);\n            params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;\n            if (!childViewTouchable) {\n                params.flags += WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;\n            }\n            params.width = mWidth;\n            params.height = mHeight;\n            params.windowAnimations = 0;\n            Field tnNextViewField = mTN.getClass().getDeclaredField(\"mNextView\");\n            tnNextViewField.setAccessible(true);\n            tnNextViewField.set(mTN, toast.getView());\n\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/view/FloatWindow.java",
    "content": "package com.yhao.floatwindow.view;\n\nimport android.animation.TimeInterpolator;\nimport android.app.Application;\nimport android.content.Context;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.LayoutRes;\nimport androidx.annotation.MainThread;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.yhao.floatwindow.constant.MoveType;\nimport com.yhao.floatwindow.constant.Screen;\nimport com.yhao.floatwindow.interfaces.IFloatWindow;\nimport com.yhao.floatwindow.interfaces.PermissionListener;\nimport com.yhao.floatwindow.interfaces.ViewStateListener;\nimport com.yhao.floatwindow.util.ActivityCounter;\nimport com.yhao.floatwindow.util.DensityUtil;\nimport com.yhao.floatwindow.util.LogUtil;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Created by yhao on 2017/12/22.\n * https://github.com/yhaolpz\n */\n\npublic class FloatWindow {\n    public static int mSlideLeftMargin;\n    public static int mSlideRightMargin;\n    public static int mSlideTopMargin;\n    public static int mSlideBottomMargin;\n\n    private FloatWindow() {\n    }\n    // FIXME: 2019/06/02 监听 Activities 的生命周期，用于判断程序是否处于后台\n    public static void init(Context applicationContext) {\n        ((Application)applicationContext).registerActivityLifecycleCallbacks(new ActivityCounter());\n    }\n\n    private static final String mDefaultTag = \"default_float_window_tag\";\n    private static Map<String, IFloatWindow> mFloatWindowMap;\n\n    public static IFloatWindow get() {\n        return get(mDefaultTag);\n    }\n\n    public static IFloatWindow get(@NonNull String tag) {\n        return mFloatWindowMap == null ? null : mFloatWindowMap.get(tag);\n    }\n\n    @MainThread\n    public static BuildFloatWindow with(@NonNull Context applicationContext) {\n        return new BuildFloatWindow(applicationContext);\n    }\n\n    public static void destroy() {\n        destroy(mDefaultTag);\n    }\n\n    public static void destroy(String tag) {\n        if (mFloatWindowMap == null || !mFloatWindowMap.containsKey(tag)) {\n            return;\n        }\n        mFloatWindowMap.get(tag).dismiss();\n        mFloatWindowMap.remove(tag);\n    }\n\n    public static class BuildFloatWindow {\n        Context mApplicationContext;\n        View mView;\n        private int mLayoutId;\n        int mWidth = ViewGroup.LayoutParams.WRAP_CONTENT;\n        int mHeight = ViewGroup.LayoutParams.WRAP_CONTENT;\n        int gravity = Gravity.TOP | Gravity.START;\n        int xOffset;\n        int yOffset;\n        boolean mShow = true;\n        Class[] mActivities;\n        int mMoveType = MoveType.slide;\n        int screenWidth;\n        int screenHeight;\n        int slideMargin;\n        long mDuration = 300;\n        TimeInterpolator mInterpolator;\n        private String mTag = mDefaultTag;\n        boolean mDesktopShow;\n        boolean mTouchable;\n        PermissionListener mPermissionListener;\n        ViewStateListener mViewStateListener;\n\n        private BuildFloatWindow() {\n        }\n\n        BuildFloatWindow(Context applicationContext) {\n            mApplicationContext = applicationContext;\n            screenWidth = DensityUtil.getScreenWidth(mApplicationContext);\n            screenHeight = DensityUtil.getScreenHeight(mApplicationContext);\n        }\n\n        public BuildFloatWindow setView(@NonNull View view) {\n            mView = view;\n            return this;\n        }\n\n        public BuildFloatWindow setView(@LayoutRes int layoutId) {\n            mLayoutId = layoutId;\n            return this;\n        }\n\n        public BuildFloatWindow setWidth(int width) {\n            mWidth = DensityUtil.dip2px(mApplicationContext, width);\n            slideMargin = width;\n            return this;\n        }\n\n        public BuildFloatWindow setHeight(int height) {\n            mHeight = DensityUtil.dip2px(mApplicationContext, height);\n            return this;\n        }\n\n        public BuildFloatWindow setWidth(@Screen.screenType int screenType, float ratio) {\n            mWidth = (int) ((screenType == Screen.width ? screenWidth : screenHeight) * ratio);\n            //LogUtil.e(\"得到宽度：\" + mWidth );\n            return this;\n        }\n\n        public BuildFloatWindow setHeight(@Screen.screenType int screenType, float ratio) {\n            mHeight = (int) ((screenType == Screen.width ? screenWidth : screenHeight) * ratio);\n            return this;\n        }\n\n        public int getWidth() {\n            return mWidth;\n        }\n\n        public int getHeight() {\n            return mHeight;\n        }\n\n        public BuildFloatWindow setX(int x) {\n            xOffset = x;\n            return this;\n        }\n\n        public BuildFloatWindow setY(int y) {\n            yOffset = y;\n            return this;\n        }\n\n        public BuildFloatWindow setX(@Screen.screenType int screenType, float ratio) {\n            xOffset = (int) ((screenType == Screen.width ? screenWidth : screenHeight) * ratio);\n            return this;\n        }\n\n        public BuildFloatWindow setY(@Screen.screenType int screenType, float ratio) {\n            yOffset = (int) ((screenType == Screen.width ? screenWidth : screenHeight) * ratio);\n            return this;\n        }\n\n        /**\n         * 设置 Activity 过滤器，用于指定在哪些界面显示悬浮窗，默认全部界面都显示\n         *\n         * @param show       　过滤类型,子类类型也会生效\n         * @param activities 　过滤界面\n         */\n        public BuildFloatWindow setFilter(boolean show, @NonNull Class... activities) {\n            mShow = show;\n            mActivities = activities;\n            return this;\n        }\n\n        public BuildFloatWindow setMoveType(@MoveType.MOVE_TYPE int moveType) {\n            if (moveType == MoveType.slide) {\n                // return setMoveType(moveType, -slideMargin / 2, -slideMargin / 2);\n                return setMoveType(moveType, slideMargin / 2, slideMargin / 2, slideMargin / 2,slideMargin / 2);\n            } else {\n                // return setMoveType(moveType, 0, 0);\n                return setMoveType(moveType, 0, 0,0,0);\n            }\n        }\n\n        /**\n         * 设置带边距的贴边动画，只有 moveType 为 MoveType.slide，设置边距才有意义，这个方法不标准，后面调整\n         *\n         * @param moveType         贴边动画 MoveType.slide\n         * @param slideLeftMargin  贴边动画左边距，默认为 0\n         * @param slideRightMargin 贴边动画右边距，默认为 0\n         */\n        public BuildFloatWindow setMoveType(@MoveType.MOVE_TYPE int moveType, int slideLeftMargin, int slideRightMargin) {\n            mMoveType = moveType;\n            mSlideLeftMargin = DensityUtil.dip2px(mApplicationContext, slideLeftMargin);\n            mSlideRightMargin = DensityUtil.dip2px(mApplicationContext, slideRightMargin);\n            //LogUtil.e(\"设置贴边距离：\" + mSlideLeftMargin + \" , \" + mSlideRightMargin );\n            return this;\n        }\n\n        public BuildFloatWindow setMoveType(@MoveType.MOVE_TYPE int moveType, int slideLeftMargin, int slideRightMargin, int slideTopMargin, int slideBottomMargin) {\n            mMoveType = moveType;\n            mSlideLeftMargin = DensityUtil.dip2px(mApplicationContext, slideLeftMargin);\n            mSlideRightMargin = DensityUtil.dip2px(mApplicationContext, slideRightMargin);\n            mSlideTopMargin = DensityUtil.dip2px(mApplicationContext, slideTopMargin);\n            mSlideBottomMargin = DensityUtil.dip2px(mApplicationContext, slideBottomMargin);\n            //LogUtil.e(\"设置贴边距离：\" + mSlideLeftMargin + \" , \" + mSlideRightMargin );\n            return this;\n        }\n\n        public BuildFloatWindow setMoveStyle(long duration, @Nullable TimeInterpolator interpolator) {\n            mDuration = duration;\n            mInterpolator = interpolator;\n            return this;\n        }\n\n        public BuildFloatWindow setTag(@NonNull String tag) {\n            mTag = tag;\n            return this;\n        }\n\n        public BuildFloatWindow setDesktopShow(boolean show) {\n            mDesktopShow = show;\n            return this;\n        }\n\n        public BuildFloatWindow setPermissionListener(PermissionListener listener) {\n            mPermissionListener = listener;\n            return this;\n        }\n\n        public BuildFloatWindow setViewStateListener(ViewStateListener listener) {\n            mViewStateListener = listener;\n            return this;\n        }\n\n        public BuildFloatWindow setChildViewTouchable(Boolean touchable) {\n            mTouchable = touchable;\n            return this;\n        }\n\n        public void build() {\n            if (mFloatWindowMap == null) {\n                mFloatWindowMap = new HashMap<>(16);\n            }\n            if (mFloatWindowMap.containsKey(mTag)) {\n                LogUtil.e(\"FloatWindow of this tag has been added, Please set a new tag for the new FloatWindow\");\n                return;\n            }\n            if (mView == null && mLayoutId == 0) {\n                throw new IllegalArgumentException(\"View has not been set!\");\n            }\n            if (mView == null) {\n                mView = DensityUtil.inflate(mApplicationContext, mLayoutId);\n            }\n            IFloatWindow floatWindowImpl = new IFloatWindowImpl(this, mTouchable);\n            mFloatWindowMap.put(mTag, floatWindowImpl);\n        }\n    }\n}\n"
  },
  {
    "path": "floatwindow/src/main/java/com/yhao/floatwindow/view/IFloatWindowImpl.java",
    "content": "package com.yhao.floatwindow.view;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.ObjectAnimator;\nimport android.animation.PropertyValuesHolder;\nimport android.animation.TimeInterpolator;\nimport android.animation.ValueAnimator;\nimport android.annotation.SuppressLint;\nimport android.os.Build;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewConfiguration;\nimport android.view.animation.DecelerateInterpolator;\n\nimport com.yhao.floatwindow.base.FloatLifecycle;\nimport com.yhao.floatwindow.constant.MoveType;\nimport com.yhao.floatwindow.constant.Screen;\nimport com.yhao.floatwindow.interfaces.FloatView;\nimport com.yhao.floatwindow.interfaces.IFloatWindow;\nimport com.yhao.floatwindow.interfaces.LifecycleListener;\nimport com.yhao.floatwindow.util.DensityUtil;\nimport com.yhao.floatwindow.util.LogUtil;\n\n/**\n * @author yhao\n * @date 2017/12/22\n * https://github.com/yhaolpz\n */\n\npublic class IFloatWindowImpl implements IFloatWindow, LifecycleListener {\n\n    private FloatWindow.BuildFloatWindow mBuildFloatWindow;\n    private FloatView mFloatView;\n    private ValueAnimator mAnimator;\n    private TimeInterpolator mDecelerateInterpolator;\n    private float downX, downY, upX, upY;\n    private int mSlop;\n    private int screenWidth;\n    private int screenHeight;\n    private boolean isShow = true;\n    private boolean mClick;\n    private boolean isHideByUser;\n    private boolean isLandscape;\n    private int statusBarHeight;\n\n    IFloatWindowImpl(final FloatWindow.BuildFloatWindow buildFloatWindow, Boolean childViewTouchable) {\n        mBuildFloatWindow = buildFloatWindow;\n\n        int baseWidth = DensityUtil.getScreenWidth(mBuildFloatWindow.mApplicationContext);\n        int baseHeight = DensityUtil.getScreenHeight(mBuildFloatWindow.mApplicationContext);\n        statusBarHeight = DensityUtil.getStatusBarHeight(mBuildFloatWindow.mApplicationContext);\n        // LogUtil.e(\"状态栏高度：\" + statusBarHeight);\n        screenWidth = baseWidth > baseHeight ? baseHeight : baseWidth;\n        screenHeight = baseWidth > baseHeight ? baseWidth : baseHeight;\n        isLandscape = baseWidth > baseHeight;\n\n        mSlop = ViewConfiguration.get(mBuildFloatWindow.mApplicationContext).getScaledTouchSlop();\n        if (mBuildFloatWindow.mMoveType == MoveType.fixed) {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {\n                mFloatView = new FloatPhone(buildFloatWindow.mApplicationContext, mBuildFloatWindow.mPermissionListener, childViewTouchable);\n            } else {\n                mFloatView = new FloatToast(buildFloatWindow.mApplicationContext, childViewTouchable);\n            }\n        } else {\n            mFloatView = new FloatPhone(buildFloatWindow.mApplicationContext, mBuildFloatWindow.mPermissionListener, childViewTouchable);\n            initTouchEvent();\n        }\n\n        mFloatView.setSize(mBuildFloatWindow.mWidth, mBuildFloatWindow.mHeight);\n        mFloatView.setGravity(mBuildFloatWindow.gravity, mBuildFloatWindow.xOffset, mBuildFloatWindow.yOffset);\n        mFloatView.setView(mBuildFloatWindow.mView);\n        mFloatView.init();\n        new FloatLifecycle(mBuildFloatWindow.mApplicationContext, mBuildFloatWindow.mShow, mBuildFloatWindow.mActivities, this);\n    }\n\n    @Override\n    public void hideByUser() {\n        if (!isShow) {\n            return;\n        }\n        mBuildFloatWindow.mView.setVisibility(View.INVISIBLE);\n        isShow = false;\n        isHideByUser = true;\n        if (mBuildFloatWindow.mViewStateListener != null) {\n            mBuildFloatWindow.mViewStateListener.onHideByUser();\n        }\n    }\n\n    @Override\n    public void showByUser() {\n        if (isShow) {\n            return;\n        }\n        mBuildFloatWindow.mView.setVisibility(View.VISIBLE);\n        isShow = true;\n        isHideByUser = false;\n        if (mBuildFloatWindow.mViewStateListener != null) {\n            mBuildFloatWindow.mViewStateListener.onShowByUser();\n        }\n    }\n\n    @Override\n    public boolean isShowing() {\n        return isShow;\n    }\n\n    /**\n     *  这里不应该直接被使用者调用，因为 FloatWindow 内保存了所有悬浮窗信息，没有同步去掉\n     */\n    @Override\n    public void dismiss() {\n        mFloatView.dismiss();\n        isShow = false;\n        if (mBuildFloatWindow.mViewStateListener != null) {\n            mBuildFloatWindow.mViewStateListener.onDismiss();\n        }\n    }\n\n    @Override\n    public void updateX(int x) {\n        checkMoveType();\n        mBuildFloatWindow.xOffset = x;\n        mFloatView.updateX(x);\n        LogUtil.e(\"设置updateX距离：\" + x + \" , \"  );\n    }\n\n    @Override\n    public void updateY(int y) {\n        checkMoveType();\n        mBuildFloatWindow.yOffset = y;\n        mFloatView.updateY(y);\n    }\n\n    @Override\n    public void updateX(int screenType, float ratio) {\n        checkMoveType();\n        mBuildFloatWindow.xOffset = (int) ((screenType == Screen.width ? screenWidth : screenHeight) * ratio);\n        mFloatView.updateX(mBuildFloatWindow.xOffset);\n        LogUtil.e(\"设置updateX距离22：\" + mBuildFloatWindow.xOffset + \" , \"  );\n    }\n\n    @Override\n    public void updateY(int screenType, float ratio) {\n        checkMoveType();\n        mBuildFloatWindow.yOffset = (int) ((screenType == Screen.width ? screenWidth : screenHeight) * ratio);\n        mFloatView.updateY(mBuildFloatWindow.yOffset);\n    }\n\n    @Override\n    public int getX() {\n        return mFloatView.getX();\n    }\n\n    @Override\n    public int getY() {\n        return mFloatView.getY();\n    }\n\n    @Override\n    public void onShow() {\n        if (isShow || isHideByUser) {\n            return;\n        }\n        mBuildFloatWindow.mView.setVisibility(View.VISIBLE);\n        isShow = true;\n        if (mBuildFloatWindow.mViewStateListener != null) {\n            mBuildFloatWindow.mViewStateListener.onShow();\n        }\n    }\n\n    @Override\n    public void onHide() {\n        if (!isShow || isHideByUser) {\n            return;\n        }\n        mBuildFloatWindow.mView.setVisibility(View.INVISIBLE);\n        isShow = false;\n        if (mBuildFloatWindow.mViewStateListener != null) {\n            mBuildFloatWindow.mViewStateListener.onHide();\n        }\n    }\n\n    @Override\n    public void onBackToDesktop() {\n        if (!mBuildFloatWindow.mDesktopShow) {\n            onHide();\n        }\n        if (mBuildFloatWindow.mViewStateListener != null) {\n            mBuildFloatWindow.mViewStateListener.onBackToDesktop();\n        }\n    }\n\n    @Override\n    public void onPortrait() {\n        double ratio = (double) getY() / screenWidth;\n        int width = mBuildFloatWindow.getWidth();\n        int x = getX() + mBuildFloatWindow.getWidth() / 2 < screenHeight / 2 ? + FloatWindow.mSlideLeftMargin : screenWidth - width - FloatWindow.mSlideRightMargin;\n//        LogUtil.e(\"得到竖屏 x 位置：\" + x );\n        mFloatView.updateXY(x, (int) (ratio * screenHeight));\n        isLandscape = false;\n    }\n\n    @Override\n    public void onLandscape() {\n        double ratio = (double) getY() / screenHeight;\n        int width = mBuildFloatWindow.getWidth();\n        int x = getX() + mBuildFloatWindow.getWidth() / 2 < screenWidth / 2 ? + FloatWindow.mSlideLeftMargin : screenHeight - width - FloatWindow.mSlideRightMargin;\n//        LogUtil.e(\"得到横屏 x 位置：\" + x );\n        mFloatView.updateXY(x, (int) (ratio * screenWidth));\n        isLandscape = true;\n    }\n\n    private void checkMoveType() {\n        if (mBuildFloatWindow.mMoveType == MoveType.fixed) {\n            throw new IllegalArgumentException(\"FloatWindow of this tag is not allowed to move!\");\n        }\n    }\n\n    private void initTouchEvent() {\n        switch (mBuildFloatWindow.mMoveType) {\n            case MoveType.inactive:\n                break;\n            default:\n                mBuildFloatWindow.mView.setOnTouchListener(new View.OnTouchListener() {\n                    float lastX, lastY, changeX, changeY;\n                    int newX, newY;\n\n                    @SuppressLint(\"ClickableViewAccessibility\")\n                    @Override\n                    public boolean onTouch(View v, MotionEvent event) {\n                        switch (event.getAction()) {\n                            case MotionEvent.ACTION_DOWN:\n                                downX = event.getRawX();\n                                downY = event.getRawY();\n                                lastX = event.getRawX();\n                                lastY = event.getRawY();\n                                cancelAnimator();\n                                break;\n                            case MotionEvent.ACTION_MOVE:\n                                changeX = event.getRawX() - lastX;\n                                changeY = event.getRawY() - lastY;\n                                newX = (int) (getX() + changeX);\n                                newY = (int) (getY() + changeY);\n                                mFloatView.updateXY(newX, newY);\n                                if (mBuildFloatWindow.mViewStateListener != null) {\n                                    mBuildFloatWindow.mViewStateListener.onPositionUpdate(newX, newY);\n                                }\n                                lastX = event.getRawX();\n                                lastY = event.getRawY();\n                                break;\n                            case MotionEvent.ACTION_UP:\n                                upX = event.getRawX();\n                                upY = event.getRawY();\n                                mClick = (Math.abs(upX - downX) > mSlop) || (Math.abs(upY - downY) > mSlop);\n                                onActionUp();\n                                return mClick;\n                            default:\n                                break;\n                        }\n                        return false;\n                    }\n                });\n        }\n    }\n\n    private void onActionUp() {\n        PropertyValuesHolder pvhX;\n        PropertyValuesHolder pvhY;\n        switch (mBuildFloatWindow.mMoveType) {\n            case MoveType.slide:\n                int[] a = new int[2];\n                mBuildFloatWindow.mView.getLocationOnScreen(a);\n                int startX = a[0];\n                int endX;\n                int startY = a[1];\n                int endY;\n                int width = mBuildFloatWindow.getWidth();\n                int height = mBuildFloatWindow.getHeight();\n                if (isLandscape) {\n                    endX = startX + width / 2 < screenHeight / 2 ? FloatWindow.mSlideLeftMargin : screenHeight - width - FloatWindow.mSlideRightMargin;\n                    //endY = startY + height / 2 < screenWidth / 2 ? FloatWindow.mSlideTopMargin : screenWidth - height - FloatWindow.mSlideBottomMargin;\n                    endY = startY < FloatWindow.mSlideTopMargin ? FloatWindow.mSlideTopMargin : startY > screenWidth - height - FloatWindow.mSlideBottomMargin - statusBarHeight ? screenWidth - height - FloatWindow.mSlideBottomMargin - statusBarHeight : startY;\n                } else {\n                    endX = startX + width / 2 < screenWidth / 2 ? FloatWindow.mSlideLeftMargin : screenWidth - width - FloatWindow.mSlideRightMargin;\n                    endY = startY < FloatWindow.mSlideTopMargin ? FloatWindow.mSlideTopMargin : startY > screenHeight - height - FloatWindow.mSlideBottomMargin - statusBarHeight ? screenHeight - height - FloatWindow.mSlideBottomMargin - statusBarHeight : startY;\n                }\n//                LogUtil.e(\"得到贴边距离：\" + FloatWindow.mSlideLeftMargin + \" , \" + FloatWindow.mSlideRightMargin );\n//                LogUtil.e(\"得到endX距离：\" + mBuildFloatWindow.getWidth() + \" , \" + endX );\n                if (startX == endX) {\n                    return;\n                }\n\n//                mAnimator = ObjectAnimator.ofInt(startX, endX);\n//                mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n//                    @Override\n//                    public void onAnimationUpdate(ValueAnimator animation) {\n//                        int x = (int) animation.getAnimatedValue();\n//                        mFloatView.updateX(x);\n//                        if (mBuildFloatWindow.mViewStateListener != null) {\n//                            mBuildFloatWindow.mViewStateListener.onPositionUpdate(x, (int) upY);\n//                        }\n//                    }\n//                });\n\n                pvhX = PropertyValuesHolder.ofInt(\"x\", startX, endX);\n                pvhY = PropertyValuesHolder.ofInt(\"y\", startY, endY);\n                mAnimator = ObjectAnimator.ofPropertyValuesHolder(pvhX, pvhY);\n                mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n                    @Override\n                    public void onAnimationUpdate(ValueAnimator animation) {\n                        int x = (int) animation.getAnimatedValue(\"x\");\n                        int y = (int) animation.getAnimatedValue(\"y\");\n                        mFloatView.updateXY(x, y);\n                        if (mBuildFloatWindow.mViewStateListener != null) {\n                            mBuildFloatWindow.mViewStateListener.onPositionUpdate(x, y);\n                        }\n                    }\n                });\n                startAnimator();\n                break;\n            case MoveType.back:\n                pvhX = PropertyValuesHolder.ofInt(\"x\", getX(), mBuildFloatWindow.xOffset);\n                pvhY = PropertyValuesHolder.ofInt(\"y\", getY(), mBuildFloatWindow.yOffset);\n                mAnimator = ObjectAnimator.ofPropertyValuesHolder(pvhX, pvhY);\n                mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n                    @Override\n                    public void onAnimationUpdate(ValueAnimator animation) {\n                        int x = (int) animation.getAnimatedValue(\"x\");\n                        int y = (int) animation.getAnimatedValue(\"y\");\n                        mFloatView.updateXY(x, y);\n                        if (mBuildFloatWindow.mViewStateListener != null) {\n                            mBuildFloatWindow.mViewStateListener.onPositionUpdate(x, y);\n                        }\n                    }\n                });\n                startAnimator();\n                break;\n            default:\n                break;\n        }\n    }\n\n    private void startAnimator() {\n        if (mBuildFloatWindow.mInterpolator == null) {\n            mBuildFloatWindow.mInterpolator = mDecelerateInterpolator == null ? mDecelerateInterpolator = new DecelerateInterpolator() : mDecelerateInterpolator;\n        }\n        mAnimator.setInterpolator(mBuildFloatWindow.mInterpolator);\n        mAnimator.addListener(new AnimatorListenerAdapter() {\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                mAnimator.removeAllUpdateListeners();\n                mAnimator.removeAllListeners();\n                mAnimator = null;\n                if (mBuildFloatWindow.mViewStateListener != null) {\n                    mBuildFloatWindow.mViewStateListener.onMoveAnimEnd();\n                }\n            }\n        });\n        mAnimator.setDuration(mBuildFloatWindow.mDuration).start();\n        if (mBuildFloatWindow.mViewStateListener != null) {\n            mBuildFloatWindow.mViewStateListener.onMoveAnimStart();\n        }\n    }\n\n    private void cancelAnimator() {\n        if (mAnimator != null && mAnimator.isRunning()) {\n            mAnimator.cancel();\n        }\n    }\n}\n"
  },
  {
    "path": "floatwindow/src/main/res/values/style.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources\n    xmlns:tools=\"http://schemas.android.com/tools\">\n    <style name=\"permission_PermissionActivity\" parent=\"@android:style/Theme.Translucent.NoTitleBar\">\n        <item name=\"android:statusBarColor\" tools:targetApi=\"lollipop\">@android:color/transparent</item>\n    </style>\n</resources>"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Thu Aug 20 21:46:24 CST 2020\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.1.1-all.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "## Project-wide Gradle settings.\n#\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n#\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\n# Default value: -Xmx1024m -XX:MaxPermSize=256m\n org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8\n#\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\norg.gradle.daemon=true\norg.gradle.parallel=true\norg.gradle.configondemand=true\n#Sat May 11 23:18:40 CST 2019\n\nandroid.enableD8=true\nandroid.enableJetifier=true\nandroid.useAndroidX=true\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK checked AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:userName=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution replaceUrl\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windowz variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\ngoto execute\n\n:4NT_args\n@rem Get arguments from the 4NT Shell from JP Software\nset CMD_LINE_ARGS=%$\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "luban/.gitignore",
    "content": "/build\n/src/androidTest/\n"
  },
  {
    "path": "luban/build.gradle",
    "content": "apply plugin: 'com.android.library'\n\nbuildscript {\n    repositories {\n        google()\n        maven { url \"https://jitpack.io\" }\n        jcenter()\n    }\n    dependencies {\n        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'\n    }\n}\n\nandroid {\n    compileSdkVersion 29\n\n    defaultConfig {\n        minSdkVersion 22\n        targetSdkVersion 29\n        versionCode 1\n        versionName \"1.0\"\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n}\n\ndependencies {}"
  },
  {
    "path": "luban/proguard-rules.pro",
    "content": "# Add project specific ProGuard replaceUrl here.\n# By default, the flags in this file are appended to flags specified\n# in /Users/sweetspot/Library/Android/sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class userName to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n"
  },
  {
    "path": "luban/src/androidTest/java/top/zibin/luban/ApplicationTest.java",
    "content": "package top.zibin.luban;\n\nimport android.app.Application;\nimport android.test.ApplicationTestCase;\n\n/**\n * <a href=\"http://d.android.com/tools/testing/testing_android.html\">Testing Fundamentals</a>\n */\npublic class ApplicationTest extends ApplicationTestCase<Application> {\n    public ApplicationTest() {\n        super(Application.class);\n    }\n}"
  },
  {
    "path": "luban/src/main/AndroidManifest.xml",
    "content": "<manifest package=\"top.zibin.luban\">\n\n</manifest>\n"
  },
  {
    "path": "luban/src/main/java/top/zibin/luban/Checker.java",
    "content": "package top.zibin.luban;\n\nimport android.graphics.BitmapFactory;\nimport android.util.Log;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Arrays;\n\nenum Checker {\n    SINGLE;\n\n    private static final String TAG = \"Luban\";\n\n    private static final String JPG = \".jpg\";\n\n    private final byte[] JPEG_SIGNATURE = new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF};\n\n    /**\n     * Determine if it is JPG.\n     *\n     * @param is image file input stream\n     */\n    boolean isJPG(InputStream is) {\n        return isJPG(toByteArray(is));\n    }\n\n    /**\n     * 以顺时针方向返回度数。值为0、90、180或270。\n     * Returns the degrees in clockwise. Values are 0, 90, 180, or 270.\n     */\n    int getOrientation(InputStream is) {\n        return getOrientation(toByteArray(is));\n    }\n\n    private boolean isJPG(byte[] data) {\n        if (data == null || data.length < 3) {\n            return false;\n        }\n        byte[] signatureB = new byte[]{data[0], data[1], data[2]};\n        return Arrays.equals(JPEG_SIGNATURE, signatureB);\n    }\n\n    private int getOrientation(byte[] jpeg) {\n        if (jpeg == null) {\n            return 0;\n        }\n\n        int offset = 0;\n        int length = 0;\n\n        // ISO/IEC 10918-1:1993(E)\n        while (offset + 3 < jpeg.length && (jpeg[offset++] & 0xFF) == 0xFF) {\n            int marker = jpeg[offset] & 0xFF;\n\n            // Check if the marker is a padding.\n            if (marker == 0xFF) {\n                continue;\n            }\n            offset++;\n\n            // Check if the marker is SOI or TEM.\n            if (marker == 0xD8 || marker == 0x01) {\n                continue;\n            }\n            // Check if the marker is EOI or SOS.\n            if (marker == 0xD9 || marker == 0xDA) {\n                break;\n            }\n\n            // Get the length and check if it is reasonable.\n            length = pack(jpeg, offset, 2, false);\n            if (length < 2 || offset + length > jpeg.length) {\n                Log.e(TAG, \"Invalid length\");\n                return 0;\n            }\n\n            // Break if the marker is EXIF in APP1.\n            if (marker == 0xE1 && length >= 8\n                    && pack(jpeg, offset + 2, 4, false) == 0x45786966\n                    && pack(jpeg, offset + 6, 2, false) == 0) {\n                offset += 8;\n                length -= 8;\n                break;\n            }\n\n            // Skip other markers.\n            offset += length;\n            length = 0;\n        }\n\n        // JEITA CP-3451 Exif Version 2.2\n        if (length > 8) {\n            // Identify the byte order.\n            int tag = pack(jpeg, offset, 4, false);\n            if (tag != 0x49492A00 && tag != 0x4D4D002A) {\n                Log.e(TAG, \"Invalid byte order\");\n                return 0;\n            }\n            boolean littleEndian = (tag == 0x49492A00);\n\n            // Get the offset and check if it is reasonable.\n            int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;\n            if (count < 10 || count > length) {\n                Log.e(TAG, \"Invalid offset\");\n                return 0;\n            }\n            offset += count;\n            length -= count;\n\n            // Get the count and go through all the elements.\n            count = pack(jpeg, offset - 2, 2, littleEndian);\n            while (count-- > 0 && length >= 12) {\n                // Get the tag and check if it is orientation.\n                tag = pack(jpeg, offset, 2, littleEndian);\n                if (tag == 0x0112) {\n                    int orientation = pack(jpeg, offset + 8, 2, littleEndian);\n                    switch (orientation) {\n                        case 1:\n                            return 0;\n                        case 3:\n                            return 180;\n                        case 6:\n                            return 90;\n                        case 8:\n                            return 270;\n                    }\n                    Log.e(TAG, \"Unsupported orientation\");\n                    return 0;\n                }\n                offset += 12;\n                length -= 12;\n            }\n        }\n\n        Log.e(TAG, \"Orientation not found\");\n        return 0;\n    }\n\n    String extSuffix(InputStreamProvider input) {\n        try {\n            BitmapFactory.Options options = new BitmapFactory.Options();\n            options.inJustDecodeBounds = true;\n            BitmapFactory.decodeStream(input.open(), null, options);\n            return options.outMimeType.replace(\"image/\", \".\");\n        } catch (Exception e) {\n            return JPG;\n        }\n    }\n\n    boolean needCompress(int leastCompressSize, String path) {\n        if (leastCompressSize > 0) {\n            File source = new File(path);\n            return source.exists() && source.length() > (leastCompressSize << 10);\n        }\n        return true;\n    }\n\n    private int pack(byte[] bytes, int offset, int length, boolean littleEndian) {\n        int step = 1;\n        if (littleEndian) {\n            offset += length - 1;\n            step = -1;\n        }\n\n        int value = 0;\n        while (length-- > 0) {\n            value = (value << 8) | (bytes[offset] & 0xFF);\n            offset += step;\n        }\n        return value;\n    }\n\n    private byte[] toByteArray(InputStream is) {\n        if (is == null) {\n            return new byte[0];\n        }\n\n        ByteArrayOutputStream buffer = new ByteArrayOutputStream();\n\n        int read;\n        byte[] data = new byte[4096];\n\n        try {\n            while ((read = is.read(data, 0, data.length)) != -1) {\n                buffer.write(data, 0, read);\n            }\n        } catch (Exception ignored) {\n            return new byte[0];\n        } finally {\n            try {\n                buffer.close();\n            } catch (IOException ignored) {\n            }\n        }\n\n        return buffer.toByteArray();\n    }\n}\n"
  },
  {
    "path": "luban/src/main/java/top/zibin/luban/CompressionPredicate.java",
    "content": "package top.zibin.luban;\n\n/**\n * Created on 2018/1/3 19:43\n *\n * @author andy\n *         <p>\n *         A functional interface (callback) that returns true or false for the given input path should be compressed.\n */\n\npublic interface CompressionPredicate {\n\n    /**\n     * Determine the given input path should be compressed and return a boolean.\n     *\n     * @param path input path\n     * @return the boolean result\n     */\n//    boolean apply(String path);\n    boolean apply(String path,InputStreamProvider inputStreamProvider);\n}\n"
  },
  {
    "path": "luban/src/main/java/top/zibin/luban/Engine.java",
    "content": "package top.zibin.luban;\n\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Matrix;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\n\n/**\n * Responsible for starting compress and managing active and cached resources.\n */\nclass Engine {\n    private InputStreamProvider srcImg;\n    private File tagImg;\n    private int srcWidth;\n    private int srcHeight;\n    private int maxWidth = 1080;\n    private int maxHeight = 1920;\n    private boolean focusAlpha;\n\n    Engine(InputStreamProvider srcImg, File tagImg,int maxWidth, int maxHeight, boolean focusAlpha) throws IOException {\n        this.srcImg = srcImg;\n        this.tagImg = tagImg;\n        this.focusAlpha = focusAlpha;\n\n        BitmapFactory.Options options = new BitmapFactory.Options();\n        //只读取图片，不加载到内存中\n        options.inJustDecodeBounds = true;\n        options.inSampleSize = 1;\n\n        BitmapFactory.decodeStream(srcImg.open(), null, options);\n        this.srcWidth = options.outWidth;\n        this.srcHeight = options.outHeight;\n        //Log.e(\"【宽度】1：\",srcWidth + \" \" + srcHeight);\n    }\n\n    private int computeSize() {\n        // 将长宽加1，以至于成为偶数\n        srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth;\n        srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight;\n\n        int longSide = Math.max(srcWidth, srcHeight);\n        int shortSide = Math.min(srcWidth, srcHeight);\n\n        float scale = ((float) shortSide / longSide);\n        if (scale <= 1 && scale > 0.5625) {\n            if (longSide < 1664) {\n                return 1;\n            } else if (longSide < 4990) {\n                return 2;\n            } else if (longSide > 4990 && longSide < 10240) {\n                return 4;\n            } else {\n                return longSide / 1280 == 0 ? 1 : longSide / 1280;\n            }\n        } else if (scale <= 0.5625 && scale > 0.5) {\n            return longSide / 1280 == 0 ? 1 : longSide / 1280;\n        } else {\n            return (int) Math.ceil(longSide / (1280.0 / scale));\n        }\n    }\n\n    //  旋转\n    private Bitmap rotatingImage(Bitmap bitmap, int angle) {\n        Matrix matrix = new Matrix();\n        matrix.postRotate(angle);\n        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);\n    }\n\n    // 缩放\n    private Bitmap scalingImage(Bitmap bitmap,float scale) {\n        //System.out.print(\"【宽度比值A】\" + maxWidth + \"  \" + bitmap.getHeight() + \"  \" + bitmap.getWidth() + \"  \"+ scale + \" \\n \");\n        Matrix matrix = new Matrix();\n        matrix.postScale(scale, scale);\n        return Bitmap.createBitmap(bitmap, 0, 0,\n                bitmap.getWidth(),\n                bitmap.getHeight(),\n                matrix, true);\n    }\n\n\n    Bitmap tagBitmap;\n    File compress() throws IOException {\n        if (tagImg != null && tagImg.exists()) {\n            return tagImg;\n        }\n        if (!tagImg.getParentFile().exists()) {\n            tagImg.getParentFile().mkdirs();\n        }\n\n        BitmapFactory.Options options = new BitmapFactory.Options();\n        options.inSampleSize = computeSize();\n\n        tagBitmap = BitmapFactory.decodeStream(srcImg.open(), null, options);\n        ByteArrayOutputStream stream = new ByteArrayOutputStream();\n\n        if (Checker.SINGLE.isJPG(srcImg.open())) {\n            tagBitmap = rotatingImage(tagBitmap, Checker.SINGLE.getOrientation(srcImg.open()));\n        }\n        int quality = 60;\n\n        float scale = 1f;\n        if(tagBitmap == null){\n            return tagImg;\n        }\n        // oldWidth 是经过采样后的宽度\n        int oldWidth = tagBitmap.getWidth();\n        int oldHeight = tagBitmap.getHeight();\n\n        if ( oldWidth > maxWidth && maxWidth > 0 ) {\n            scale = ((float) maxWidth) / oldWidth;\n        }\n        //Log.e(\"【宽度比值C】\", maxWidth + \"  \" + oldWidth + \" = \" + scale + \"   ,  \" + (((float) maxWidth) / oldWidth) );\n        if( oldHeight > maxHeight && maxHeight > 0 ){\n            scale = Math.min(scale,((float) maxHeight) / oldHeight );\n        }\n        //Log.e(\"【宽度比值D】\", maxHeight + \"  \" + oldHeight + \" = \" + scale + \"   ,  \" + ((float) maxHeight) / oldHeight );\n\n        if( scale != 1f ){\n            quality = 80;\n            tagBitmap = scalingImage(tagBitmap, scale );\n        }\n\n        tagBitmap.compress(focusAlpha ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, quality, stream);\n        tagBitmap.recycle();\n        tagBitmap = null;\n        srcImg = null;\n        // Bitmap.recycle();释放了图片的资源，但是Bitmap本身并没有释放，它依然在占用资源。\n        // 所以还要在调用一次Bitmap=null;将Bitmap赋空，让有向图断掉，好让GC回收。\n\n        FileOutputStream fos = new FileOutputStream(tagImg);\n        fos.write(stream.toByteArray());\n        fos.flush();\n        fos.close();\n        stream.close();\n        return tagImg;\n    }\n\n\n}"
  },
  {
    "path": "luban/src/main/java/top/zibin/luban/FileProvider.java",
    "content": "package top.zibin.luban;\n\nimport java.io.File;\n\n/**\n * 自己增加的，为的是最后能在回调中区分文件到底有没有压缩\n * @author Wizos on 2018/12/27.\n */\n\npublic class FileProvider {\n    public boolean hasCompressed = false;\n    public File file;\n}\n"
  },
  {
    "path": "luban/src/main/java/top/zibin/luban/InputStreamProvider.java",
    "content": "package top.zibin.luban;\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * 通过此接口获取输入流，以兼容文件、FileProvider方式获取到的图片\n * <p>\n * Get the input stream through this interface, and obtain the picture using compatible files and FileProvider\n */\npublic interface InputStreamProvider {\n\n    InputStream open() throws IOException;\n\n    String getPath();\n}\n"
  },
  {
    "path": "luban/src/main/java/top/zibin/luban/Luban.java",
    "content": "package top.zibin.luban;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.AsyncTask;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Message;\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.UnsupportedEncodingException;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\n\n@SuppressWarnings(\"unused\")\npublic class Luban implements Handler.Callback {\n    private static final String TAG = \"Luban\";\n    private static final String DEFAULT_DISK_CACHE_DIR = \"luban_disk_cache\";\n\n    private static final int MSG_COMPRESS_UNCHANGE = 3;\n    private static final int MSG_COMPRESS_SUCCESS = 0;\n    private static final int MSG_COMPRESS_START = 1;\n    private static final int MSG_COMPRESS_ERROR = 2;\n\n    private int maxWidth;\n    private int maxHeight;\n    private String mTargetDir;\n    private String mTargetPath; // 自加\n    private boolean focusAlpha;\n    private int mLeastCompressSize;\n    private OnRenameListener mRenameListener;\n    private OnCompressListener mCompressListener;\n    private CompressionPredicate mCompressionPredicate;\n    private List<InputStreamProvider> mStreamProviders;\n\n    private Handler mHandler;\n\n    private Luban(Builder builder) {\n        // 自加\n        this.maxWidth = builder.maxWidth;\n        this.maxHeight = builder.maxHeight;\n        this.mTargetPath = builder.mTargetPath;\n\n        this.mTargetDir = builder.mTargetDir;\n        this.mRenameListener = builder.mRenameListener;\n        this.mStreamProviders = builder.mStreamProviders;\n        this.mCompressListener = builder.mCompressListener;\n        this.mLeastCompressSize = builder.mLeastCompressSize;\n        this.mCompressionPredicate = builder.mCompressionPredicate;\n        mHandler = new Handler(Looper.getMainLooper(), this);\n    }\n\n    public static Builder with(Context context) {\n        return new Builder(context);\n    }\n\n    /**\n     * Returns a file with a cache image name in the private cache directory.\n     *\n     * @param context A context.\n     */\n    private File getImageCacheFile(Context context, String suffix) {\n        if (TextUtils.isEmpty(mTargetDir)) {\n            mTargetDir = getImageCacheDir(context).getAbsolutePath();\n        }\n\n        String cacheBuilder = mTargetDir + \"/\" +\n                System.currentTimeMillis() +\n                (int) (Math.random() * 1000) +\n                (TextUtils.isEmpty(suffix) ? \".jpg\" : suffix);\n\n//        System.out.print(\"当前的0：\" + mTargetDir + \"   \" + cacheBuilder);\n        return new File(cacheBuilder);\n    }\n\n    // 自加\n    private File getImageCacheFile(Context context, InputStreamProvider input, String suffix) {\n        if (TextUtils.isEmpty(mTargetDir)) {\n            mTargetDir = getImageCacheDir(context).getAbsolutePath();\n        }\n\n        String cacheBuilder = mTargetDir + \"/\" +\n                md5(input.getPath()) +\n                (TextUtils.isEmpty(suffix) ? \".jpg\" : suffix);\n\n        //System.out.print(\"当前的1：\" + mTargetDir + \"   \" + cacheBuilder + \"   \" + input.getPath() + \"   \"  + suffix );\n        return new File(cacheBuilder);\n    }\n\n    private File getImageCacheFile(Context context, InputStreamProvider input, String suffix, String mTargetPath) {\n        if (TextUtils.isEmpty(mTargetPath)) {\n            return getImageCacheFile(context, input, suffix);\n        }\n        //System.out.print(\"当前的2：\" + mTargetDir + \"  --  \" + suffix + \"  == \" + mTargetPath);\n        return new File(mTargetPath);\n    }\n\n    /**\n     * 将字符串转成MD5值\n     *\n     * @param string 字符串\n     * @return MD5 后的字符串\n     */\n    private String md5(String string) {\n        byte[] hash;\n        try {\n            hash = MessageDigest.getInstance(\"MD5\").digest(string.getBytes(\"UTF-8\"));\n        } catch (NoSuchAlgorithmException e) {\n            e.printStackTrace();\n            return null;\n        } catch (UnsupportedEncodingException e) {\n            e.printStackTrace();\n            return null;\n        }\n        StringBuilder hex = new StringBuilder(hash.length * 2);\n        for (byte b : hash) {\n            if ((b & 0xFF) < 0x10) {\n                hex.append(\"0\");\n            }\n            hex.append(Integer.toHexString(b & 0xFF));\n        }\n        return hex.toString();\n    }\n\n//  public String getPreSavePath(){\n//    getImageCacheFile(context, Checker.SINGLE.extSuffix(input));\n//  }\n\n\n    private File getImageCustomFile(Context context, String filename) {\n        if (TextUtils.isEmpty(mTargetDir)) {\n            mTargetDir = getImageCacheDir(context).getAbsolutePath();\n        }\n\n        String cacheBuilder = mTargetDir + \"/\" + filename;\n\n        return new File(cacheBuilder);\n    }\n\n    /**\n     * Returns a directory with a default name in the private cache directory of the application to\n     * use to store retrieved audio.\n     *\n     * @param context A context.\n     * @see #getImageCacheDir(Context, String)\n     */\n    private File getImageCacheDir(Context context) {\n        return getImageCacheDir(context, DEFAULT_DISK_CACHE_DIR);\n    }\n\n    /**\n     * Returns a directory with the given name in the private cache directory of the application to\n     * use to store retrieved media and thumbnails.\n     *\n     * @param context   A context.\n     * @param cacheName The name of the subdirectory in which to store the cache.\n     * @see #getImageCacheDir(Context)\n     */\n    private static File getImageCacheDir(Context context, String cacheName) {\n        File cacheDir = context.getExternalCacheDir();\n        if (cacheDir != null) {\n            File result = new File(cacheDir, cacheName);\n            if (!result.mkdirs() && (!result.exists() || !result.isDirectory())) {\n                // File wasn't able to create a directory, or the result exists but not a directory\n                return null;\n            }\n            return result;\n        }\n        if (Log.isLoggable(TAG, Log.ERROR)) {\n            Log.e(TAG, \"default disk cache dir is null\");\n        }\n        return null;\n    }\n\n    /**\n     * start asynchronous compress thread\n     */\n    private void launch(final Context context) {\n        if (mStreamProviders == null || mStreamProviders.size() == 0 && mCompressListener != null) {\n            mCompressListener.onError(new NullPointerException(\"image file cannot be null\"));\n        }\n\n        Iterator<InputStreamProvider> iterator = mStreamProviders.iterator();\n\n        while (iterator.hasNext()) {\n            final InputStreamProvider path = iterator.next();\n\n            AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {\n                @Override\n                public void run() {\n                    try {\n                        mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_START));\n                        FileProvider result = compress(context, path);\n                        //Log.e(\"返回结果\" , result.hasCompressed + \"\" );\n                        if( result.hasCompressed ){\n                            mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_SUCCESS, result.file));\n                        }else {\n                            mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_UNCHANGE, result.file));\n                        }\n                    } catch (IOException e) {\n                        mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_ERROR, e));\n                    }\n                }\n            });\n\n            iterator.remove();\n        }\n    }\n\n    /**\n     * start compress and return the file\n     */\n    private File get(InputStreamProvider input, Context context) throws IOException {\n        return new Engine(input, getImageCacheFile(context, input, Checker.SINGLE.extSuffix(input)),maxWidth,maxHeight, focusAlpha).compress();\n//    return new Engine(input, getImageCacheFile(context,input, Checker.SINGLE.extSuffix(input),mTargetPath), focusAlpha).compress();\n    }\n\n    private List<File> get(Context context) throws IOException {\n        List<File> results = new ArrayList<>();\n        Iterator<InputStreamProvider> iterator = mStreamProviders.iterator();\n\n        while (iterator.hasNext()) {\n            results.add(compress(context, iterator.next()).file);\n            iterator.remove();\n        }\n\n        return results;\n    }\n\n    /**\n     * 自己新增加的函数\n     * @param context\n     * @param path\n     * @return\n     * @throws IOException\n     */\n    private FileProvider compress(Context context, InputStreamProvider path) throws IOException {\n        FileProvider fileProvider = new FileProvider();\n\n        File outFile = getImageCacheFile(context, path, Checker.SINGLE.extSuffix(path));\n        if (mRenameListener != null) {\n            String filename = mRenameListener.rename(path.getPath());\n            outFile = getImageCustomFile(context, filename);\n        }\n\n        if (mCompressionPredicate != null) {\n            if ( mCompressionPredicate.apply(path.getPath(),path) && Checker.SINGLE.needCompress(mLeastCompressSize, path.getPath())  ) {\n                fileProvider.file = new Engine(path, outFile,maxWidth,maxHeight, focusAlpha).compress();\n                fileProvider.hasCompressed = true;\n                //Log.e(\"检测：\",\"要压缩，\" + mCompressionPredicate.apply(path.getPath(),path)  + Checker.SINGLE.needCompress(mLeastCompressSize, path.getPath()));\n            } else {\n                fileProvider.file = new File(path.getPath());\n                fileProvider.hasCompressed = false;\n                //Log.e(\"检测：\",\"不要压缩，\" + mCompressionPredicate.apply(path.getPath(),path)  + Checker.SINGLE.needCompress(mLeastCompressSize, path.getPath()) );\n            }\n        } else {\n            if( Checker.SINGLE.needCompress(mLeastCompressSize, path.getPath()) ){\n                fileProvider.file = new Engine(path, outFile,maxWidth,maxHeight, focusAlpha).compress();\n                fileProvider.hasCompressed = true;\n            }else {\n                fileProvider.file = new File(path.getPath());\n                fileProvider.hasCompressed = false;\n            }\n        }\n\n        return fileProvider;\n    }\n\n//    private File compress(Context context, InputStreamProvider path) throws IOException {\n//        File result;\n//\n//        File outFile = getImageCacheFile(context, path, Checker.SINGLE.extSuffix(path));\n////    File outFile = getImageCacheFile(context,path, Checker.SINGLE.extSuffix(path),mTargetPath);\n//\n//        if (mRenameListener != null) {\n//            String filename = mRenameListener.rename(path.getPath());\n//            outFile = getImageCustomFile(context, filename);\n//        }\n//\n//\n//        if (mCompressionPredicate != null) {\n//            if ( mCompressionPredicate.apply(path.getPath()) && Checker.SINGLE.needCompress(mLeastCompressSize, path.getPath()) ) {\n//                result = new Engine(path, outFile, focusAlpha).compress();\n//            } else {\n//                result = new File(path.getPath());\n//            }\n//        } else {\n//            result = Checker.SINGLE.needCompress(mLeastCompressSize, path.getPath()) ?\n//                    new Engine(path, outFile, focusAlpha).compress() :\n//                    new File(path.getPath());\n//        }\n//\n//        return result;\n//    }\n\n    @Override\n    public boolean handleMessage(Message msg) {\n        if (mCompressListener == null) return false;\n\n        switch (msg.what) {\n            case MSG_COMPRESS_START:\n                mCompressListener.onStart();\n                break;\n            case MSG_COMPRESS_SUCCESS:\n                mCompressListener.onSuccess((File) msg.obj);\n                break;\n            case MSG_COMPRESS_UNCHANGE:\n                mCompressListener.onUnChange((File) msg.obj);\n                break;\n            case MSG_COMPRESS_ERROR:\n                mCompressListener.onError((Throwable) msg.obj);\n                break;\n        }\n        return false;\n    }\n\n    public static class Builder {\n        private Context context;\n        private String mTargetDir;\n        private int maxWidth;\n        private int maxHeight;\n        private boolean focusAlpha;\n        private int mLeastCompressSize = 100;\n        private OnRenameListener mRenameListener;\n        private OnCompressListener mCompressListener;\n        private CompressionPredicate mCompressionPredicate;\n        private List<InputStreamProvider> mStreamProviders;\n\n        Builder(Context context) {\n            this.context = context;\n            this.mStreamProviders = new ArrayList<>();\n        }\n\n        private Luban build() {\n            return new Luban(this);\n        }\n\n        public Builder load(InputStreamProvider inputStreamProvider) {\n            mStreamProviders.add(inputStreamProvider);\n            return this;\n        }\n\n        public Builder load(final File file) {\n            mStreamProviders.add(new InputStreamProvider() {\n                @Override\n                public InputStream open() throws IOException {\n                    return new FileInputStream(file);\n                }\n\n                @Override\n                public String getPath() {\n                    return file.getAbsolutePath();\n                }\n            });\n            return this;\n        }\n\n\n        private String mTargetPath;\n\n        public Builder setTargetPath(String mTargetPath) {\n            this.mTargetPath = mTargetPath;\n            return this;\n        }\n\n        public Builder load(final String string) {\n            mStreamProviders.add(new InputStreamProvider() {\n                @Override\n                public InputStream open() throws IOException {\n                    return new FileInputStream(string);\n                }\n\n                @Override\n                public String getPath() {\n                    return string;\n                }\n            });\n            return this;\n        }\n\n        public <T> Builder load(List<T> list) {\n            for (T src : list) {\n                if (src instanceof String) {\n                    load((String) src);\n                } else if (src instanceof File) {\n                    load((File) src);\n                } else if (src instanceof Uri) {\n                    load((Uri) src);\n                } else {\n                    throw new IllegalArgumentException(\"Incoming data type exception, it must be String, File, Uri or Bitmap\");\n                }\n            }\n            return this;\n        }\n\n        public Builder load(final Uri uri) {\n            mStreamProviders.add(new InputStreamProvider() {\n                @Override\n                public InputStream open() throws IOException {\n                    return context.getContentResolver().openInputStream(uri);\n                }\n\n                @Override\n                public String getPath() {\n                    return uri.getPath();\n                }\n            });\n            return this;\n        }\n\n        public Builder putGear(int gear) {\n            return this;\n        }\n\n        public Builder setRenameListener(OnRenameListener listener) {\n            this.mRenameListener = listener;\n            return this;\n        }\n\n        public Builder setCompressListener(OnCompressListener listener) {\n            this.mCompressListener = listener;\n            return this;\n        }\n\n        public Builder setTargetDir(String targetDir) {\n            this.mTargetDir = targetDir;\n            return this;\n        }\n\n        /**\n         * 自己添加的，用于控制压缩出来的图片的尺寸大小\n         * @param maxWidth\n         * @return\n         */\n        public Builder setMaxSiz(int maxWidth, int maxHeight) {\n            this.maxWidth = maxWidth;\n            this.maxHeight = maxHeight;\n            return this;\n        }\n\n        /**\n         * Do I need to keep the image's alpha channel\n         *\n         * @param focusAlpha <p> true - to keep alpha channel, the compress speed will be slow. </p>\n         *                   <p> false - don't keep alpha channel, it might have a black background.</p>\n         */\n        public Builder setFocusAlpha(boolean focusAlpha) {\n            this.focusAlpha = focusAlpha;\n            return this;\n        }\n\n        /**\n         * do not compress when the origin image file size less than one value\n         *\n         * @param size the value of file size, unit KB, default 100K\n         */\n        public Builder ignoreBy(int size) {\n            this.mLeastCompressSize = size;\n            return this;\n        }\n\n        /**\n         * do compress image when return value was true, otherwise, do not compress the image file\n         *\n         * @param compressionPredicate A predicate callback that returns true or false for the given input path should be compressed.\n         */\n        public Builder filter(CompressionPredicate compressionPredicate) {\n            this.mCompressionPredicate = compressionPredicate;\n            return this;\n        }\n\n\n        /**\n         * begin compress image with asynchronous\n         */\n        public void launch() {\n            build().launch(context);\n        }\n\n        public File get(final String path) throws IOException {\n            return build().get(new InputStreamProvider() {\n                @Override\n                public InputStream open() throws IOException {\n                    return new FileInputStream(path);\n                }\n\n                @Override\n                public String getPath() {\n                    return path;\n                }\n            }, context);\n        }\n\n        /**\n         * begin compress image with synchronize\n         *\n         * @return the thumb image file list\n         */\n        public List<File> get() throws IOException {\n            return build().get(context);\n        }\n    }\n}"
  },
  {
    "path": "luban/src/main/java/top/zibin/luban/OnCompressListener.java",
    "content": "package top.zibin.luban;\n\nimport java.io.File;\n\npublic interface OnCompressListener {\n\n    /**\n     * Fired when the compression is started, override to handle in your own code\n     */\n    void onStart();\n\n    /**\n     * Fired when a compression returns successfully, override to handle in your own code\n     */\n    void onSuccess(File file);\n\n    /**\n     * 自己增加的\n     * 条件不符合，最后没有压缩\n     */\n    void onUnChange(File file);\n    /**\n     * Fired when a compression fails to complete, override to handle in your own code\n     */\n    void onError(Throwable e);\n}\n"
  },
  {
    "path": "luban/src/main/java/top/zibin/luban/OnRenameListener.java",
    "content": "package top.zibin.luban;\n\n/**\n * Author: zibin\n * Datetime: 2018/5/18\n * <p>\n * 提供修改压缩图片命名接口\n * <p>\n * A functional interface (callback) that used to rename the file after compress.\n */\npublic interface OnRenameListener {\n\n    /**\n     * 压缩前调用该方法用于修改压缩后文件名\n     * <p>\n     * Call before compression begins.\n     *\n     * @param filePath 传入文件路径/ file path\n     * @return 返回重命名后的字符串/ file name\n     */\n    String rename(String filePath);\n}\n"
  },
  {
    "path": "luban/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">Luban</string>\n</resources>\n"
  },
  {
    "path": "privacy_and_security.md",
    "content": "- Loread has no server to save your data.\n- Loread uses RSS server APIs to get feeds data for you.\n- Loread uses the Google Firebase library to improve app crash issues and performance. This only transmits app crash logs.\n"
  },
  {
    "path": "settings.gradle",
    "content": "include ':app', ':swipelayout', ':luban', \":support\",':floatwindow',':agentweb-core'"
  },
  {
    "path": "support/.gitignore",
    "content": "/build"
  },
  {
    "path": "support/build.gradle",
    "content": "apply plugin: \"com.android.library\"\n\nandroid {\n    compileSdkVersion 29\n    buildToolsVersion \"29.0.3\"\n\n    defaultConfig {\n        minSdkVersion 22\n        targetSdkVersion 29\n        consumerProguardFiles 'proguard-rules.txt'\n    }\n\n    resourcePrefix 'support_recycler'\n}\n\ndependencies {\n    api 'androidx.recyclerview:recyclerview:1.1.0'\n}"
  },
  {
    "path": "support/proguard-rules.txt",
    "content": "#-keepclasseswithmembers class android.support.v7.widget.RecyclerView$ViewHolder {\n#   public final android.view.View *;\n#}"
  },
  {
    "path": "support/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright 2017 Yan Zhenjie\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distribucheckedd on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n-->\n<manifest package=\"com.yanzhenjie.recyclerview\"/>"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/AdapterWrapper.java",
    "content": "/*\n * Copyright 2017 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview;\n\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.collection.SparseArrayCompat;\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.StaggeredGridLayoutManager;\n\nimport java.lang.reflect.Field;\nimport java.util.List;\n\nimport static com.yanzhenjie.recyclerview.SwipeRecyclerView.LEFT_DIRECTION;\nimport static com.yanzhenjie.recyclerview.SwipeRecyclerView.RIGHT_DIRECTION;\n\n/**\n * Created by YanZhenjie on 2017/7/20.\n */\nclass AdapterWrapper extends RecyclerView.Adapter<RecyclerView.ViewHolder> {\n\n    private static final int BASE_ITEM_TYPE_HEADER = 100000;\n    private static final int BASE_ITEM_TYPE_FOOTER = 200000;\n\n    private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();\n    private SparseArrayCompat<View> mFootViews = new SparseArrayCompat<>();\n\n    private RecyclerView.Adapter mAdapter;\n    private LayoutInflater mInflater;\n\n    private SwipeMenuCreator mSwipeMenuCreator;\n    private OnItemMenuClickListener mOnItemMenuClickListener;\n    private OnItemClickListener mOnItemClickListener;\n    private OnItemLongClickListener mOnItemLongClickListener;\n\n    // TODO: 2019/4/14 新增\n    private OnItemSwipeListener mOnItemSwipeListener;\n\n    AdapterWrapper(Context context, RecyclerView.Adapter adapter) {\n        this.mInflater = LayoutInflater.from(context);\n        this.mAdapter = adapter;\n    }\n\n    public RecyclerView.Adapter getOriginAdapter() {\n        return mAdapter;\n    }\n\n    void setSwipeMenuCreator(SwipeMenuCreator swipeMenuCreator) {\n        this.mSwipeMenuCreator = swipeMenuCreator;\n    }\n\n    void setOnItemMenuClickListener(OnItemMenuClickListener listener) {\n        this.mOnItemMenuClickListener = listener;\n    }\n\n    void setOnItemClickListener(OnItemClickListener listener) {\n        this.mOnItemClickListener = listener;\n    }\n\n    void setOnItemLongClickListener(OnItemLongClickListener listener) {\n        this.mOnItemLongClickListener = listener;\n    }\n\n    // TODO: 2019/4/14 新增\n    void setOnItemSwipeListener(OnItemSwipeListener listener) {\n//        Log.e(\"Adapter\", \"设置监听器\" + listener );\n        this.mOnItemSwipeListener = listener;\n    }\n\n    /**\n     * 包含 header 和 footer 的数量\n     * @return\n     */\n    @Override\n    public int getItemCount() {\n        return getHeaderCount() + getContentItemCount() + getFooterCount();\n    }\n\n    private int getContentItemCount() {\n        return mAdapter.getItemCount();\n    }\n\n    @Override\n    public int getItemViewType(int position) {\n        if (isHeader(position)) {\n            return mHeaderViews.keyAt(position);\n        } else if (isFooter(position)) {\n            return mFootViews.keyAt(position - getHeaderCount() - getContentItemCount());\n        }\n        return mAdapter.getItemViewType(position - getHeaderCount());\n    }\n\n    @NonNull\n    @Override\n    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n//        Log.e(\"绑定视图\" , \"创建\");\n\n        View contentView = mHeaderViews.get(viewType);\n        if (contentView != null) {\n            return new ViewHolder(contentView);\n        }\n\n//        Log.e(\"绑定视图\" , \"创建22\");\n        contentView = mFootViews.get(viewType);\n        if (contentView != null) {\n            return new ViewHolder(contentView);\n        }\n\n        final RecyclerView.ViewHolder viewHolder = mAdapter.onCreateViewHolder(parent, viewType);\n        if (mOnItemClickListener != null) {\n            viewHolder.itemView.setOnClickListener(new View.OnClickListener() {\n                @Override\n                public void onClick(View v) {\n                    mOnItemClickListener.onItemClick(v, viewHolder.getAdapterPosition());\n                }\n            });\n        }\n        if (mOnItemLongClickListener != null) {\n            viewHolder.itemView.setOnLongClickListener(new View.OnLongClickListener() {\n                @Override\n                public boolean onLongClick(View v) {\n                    mOnItemLongClickListener.onItemLongClick(v, viewHolder.getAdapterPosition());\n                    return true;\n                }\n            });\n        }\n\n//        Log.e(\"绑定视图\", \"监听器：\" + mSwipeMenuCreator +  (viewHolder.itemView instanceof SwipeDragLayout) + mOnItemSwipeListener );\n\n        if (mSwipeMenuCreator == null) return viewHolder;\n\n        contentView = mInflater.inflate(R.layout.support_recycler_view_item2, parent, false);\n        ViewGroup viewGroup = contentView.findViewById(R.id.swipe_content);\n        viewGroup.addView(viewHolder.itemView);\n\n//        Log.e(\"绑定视图\", \"监听器：\" + (contentView instanceof SwipeDragLayout) );\n//        Log.e(\"绑定视图\", \"监听器：\" +  mOnItemSwipeListener );\n        // TODO: 2019/4/14 新增\n        if ( (contentView instanceof SwipeDragLayout) && mOnItemSwipeListener != null) {\n            SwipeDragLayout menuLayout = (SwipeDragLayout) contentView;\n            menuLayout.setListener(new SwipeDragLayout.SwipeListener() {\n                @Override\n                public void onClose(View swipeMenu, int direction) {\n                    mOnItemSwipeListener.onClose(swipeMenu, direction, viewHolder.getAdapterPosition());\n                }\n\n                @Override\n                public void onCloseLeft() {\n//                    Log.e(\"绑定视图\", \"左侧关闭\");\n                    mOnItemSwipeListener.onCloseLeft(viewHolder.getAdapterPosition());\n                }\n                @Override\n                public void onCloseRight() {\n//                    Log.e(\"绑定视图\", \"右边关闭\");\n                    mOnItemSwipeListener.onCloseRight(viewHolder.getAdapterPosition());\n                }\n            });\n        }\n\n\n\n        try {\n            Field itemView = getSupperClass(viewHolder.getClass()).getDeclaredField(\"itemView\");\n            if (!itemView.isAccessible()) itemView.setAccessible(true);\n            itemView.set(viewHolder, contentView);\n        } catch (Exception ignored) {\n        }\n        return viewHolder;\n    }\n\n    private Class<?> getSupperClass(Class<?> aClass) {\n        Class<?> supperClass = aClass.getSuperclass();\n        if (supperClass != null && !supperClass.equals(Object.class)) {\n            return getSupperClass(supperClass);\n        }\n        return aClass;\n    }\n\n    @Override\n    public final void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {\n    }\n\n    @Override\n    public final void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {\n        if (isHeaderOrFooter(holder)) return;\n\n        View itemView = holder.itemView;\n        position -= getHeaderCount();\n\n        // Log.e(\"绑定适配器\", \"   视图\" );\n        if (itemView instanceof SwipeDragLayout && mSwipeMenuCreator != null) {\n            SwipeDragLayout menuLayout = (SwipeDragLayout)itemView;\n            SwipeMenu leftMenu = new SwipeMenu(menuLayout);\n            SwipeMenu rightMenu = new SwipeMenu(menuLayout);\n            mSwipeMenuCreator.onCreateMenu(leftMenu, rightMenu, position);\n\n            SwipeMenuView leftMenuView = menuLayout.findViewById(R.id.swipe_left);\n//            Log.e(\"绑定适配器\", \"实例化 左 菜单: \" + position + \" , \" + leftMenu.hasMenuItems()  + \" , \" + leftMenuView.getChildCount());\n            if (leftMenu.hasMenuItems()) {\n                leftMenuView.setOrientation(leftMenu.getOrientation());\n                leftMenuView.createMenu(holder, leftMenu, menuLayout, LEFT_DIRECTION, mOnItemMenuClickListener);\n            } else if (leftMenuView.getChildCount() > 0) {\n                leftMenuView.removeAllViews();\n            }\n\n            SwipeMenuView rightMenuView = menuLayout.findViewById(R.id.swipe_right);\n//            Log.e(\"绑定适配器\", \"实例化 右 菜单\" + position + \" , \"+ rightMenu.hasMenuItems()  + \" , \" + rightMenuView.getChildCount());\n            if (rightMenu.hasMenuItems()) {\n                rightMenuView.setOrientation(rightMenu.getOrientation());\n                rightMenuView.createMenu(holder, rightMenu, menuLayout, RIGHT_DIRECTION, mOnItemMenuClickListener);\n            } else if (rightMenuView.getChildCount() > 0) {\n                rightMenuView.removeAllViews();\n            }\n        }\n\n\n        mAdapter.onBindViewHolder(holder, position, payloads);\n    }\n\n    @Override\n    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {\n        mAdapter.onAttachedToRecyclerView(recyclerView);\n\n        RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();\n        if (lm instanceof GridLayoutManager) {\n            final GridLayoutManager glm = (GridLayoutManager)lm;\n            final GridLayoutManager.SpanSizeLookup originLookup = glm.getSpanSizeLookup();\n\n            glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {\n                @Override\n                public int getSpanSize(int position) {\n                    if (isHeaderOrFooter(position)) return glm.getSpanCount();\n                    if (originLookup != null) return originLookup.getSpanSize(position);\n                    return 1;\n                }\n            });\n        }\n    }\n\n    @Override\n    public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) {\n        if (isHeaderOrFooter(holder)) {\n            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();\n            if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {\n                StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams)lp;\n                p.setFullSpan(true);\n            }\n        } else {\n            mAdapter.onViewAttachedToWindow(holder);\n        }\n    }\n\n    public boolean isHeaderOrFooter(RecyclerView.ViewHolder holder) {\n        if (holder instanceof ViewHolder) return true;\n\n        return isHeaderOrFooter(holder.getAdapterPosition());\n    }\n\n    public boolean isHeaderOrFooter(int position) {\n        return isHeader(position) || isFooter(position);\n    }\n\n    public boolean isHeader(int position) {\n        return position >= 0 && position < getHeaderCount();\n    }\n\n    public boolean isFooter(int position) {\n        return position >= getHeaderCount() + getContentItemCount();\n    }\n\n    public void addHeaderView(View view) {\n        mHeaderViews.put(getHeaderCount() + BASE_ITEM_TYPE_HEADER, view);\n    }\n\n    public void addHeaderViewAndNotify(View view) {\n        addHeaderView(view);\n        notifyItemInserted(getHeaderCount() - 1);\n    }\n\n    public void removeHeaderViewAndNotify(View view) {\n        int headerIndex = mHeaderViews.indexOfValue(view);\n        if (headerIndex == -1) return;\n\n        mHeaderViews.removeAt(headerIndex);\n        notifyItemRemoved(headerIndex);\n    }\n\n    public void addFooterView(View view) {\n        mFootViews.put(getFooterCount() + BASE_ITEM_TYPE_FOOTER, view);\n    }\n\n    public void addFooterViewAndNotify(View view) {\n        addFooterView(view);\n        notifyItemInserted(getHeaderCount() + getContentItemCount() + getFooterCount() - 1);\n    }\n\n    public void removeFooterViewAndNotify(View view) {\n        int footerIndex = mFootViews.indexOfValue(view);\n        if (footerIndex == -1) return;\n\n        mFootViews.removeAt(footerIndex);\n        notifyItemRemoved(getHeaderCount() + getContentItemCount() + footerIndex);\n    }\n\n    public int getHeaderCount() {\n        return mHeaderViews.size();\n    }\n\n    public int getFooterCount() {\n        return mFootViews.size();\n    }\n\n    static class ViewHolder extends RecyclerView.ViewHolder {\n\n        public ViewHolder(View itemView) {\n            super(itemView);\n        }\n    }\n\n    @Override\n    public final void setHasStableIds(boolean hasStableIds) {\n        super.setHasStableIds(hasStableIds);\n    }\n\n    @Override\n    public long getItemId(int position) {\n        if (isHeaderOrFooter(position)) {\n            return -position - 1;\n        }\n\n        position -= getHeaderCount();\n        return mAdapter.getItemId(position);\n    }\n\n    @Override\n    public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {\n        if (!isHeaderOrFooter(holder)) mAdapter.onViewRecycled(holder);\n    }\n\n    @Override\n    public boolean onFailedToRecycleView(@NonNull RecyclerView.ViewHolder holder) {\n        if (!isHeaderOrFooter(holder)) return mAdapter.onFailedToRecycleView(holder);\n        return false;\n    }\n\n    @Override\n    public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder holder) {\n        if (!isHeaderOrFooter(holder)) mAdapter.onViewDetachedFromWindow(holder);\n    }\n\n    @Override\n    public void registerAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) {\n        super.registerAdapterDataObserver(observer);\n    }\n\n    @Override\n    public void unregisterAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) {\n        super.unregisterAdapterDataObserver(observer);\n    }\n\n    @Override\n    public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {\n        mAdapter.onDetachedFromRecyclerView(recyclerView);\n    }\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/Controller.java",
    "content": "/*\n * Copyright 2016 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview;\n\n/**\n * Created by Yan Zhenjie on 2016/7/27.\n */\ninterface Controller {\n\n    /**\n     * The menu is open?\n     *\n     * @return true, otherwise false.\n     */\n    boolean isMenuOpen();\n\n    /**\n     * The menu is open on the left?\n     *\n     * @return true, otherwise false.\n     */\n//    boolean isLeftMenuOpen();\n\n    /**\n     * The menu is open on the right?\n     *\n     * @return true, otherwise false.\n     */\n//    boolean isRightMenuOpen();\n\n    /**\n     * The menu is completely open?\n     *\n     * @return true, otherwise false.\n     */\n//    boolean isCompleteOpen();\n\n    /**\n     * The menu is completely open on the left?\n     *\n     * @return true, otherwise false.\n     */\n//    boolean isLeftCompleteOpen();\n\n    /**\n     * The menu is completely open on the right?\n     *\n     * @return true, otherwise false.\n     */\n//    boolean isRightCompleteOpen();\n\n    /**\n     * The menu is open?\n     *\n     * @return true, otherwise false.\n     */\n//    boolean isMenuOpenNotEqual();\n\n    /**\n     * The menu is open on the left?\n     *\n     * @return true, otherwise false.\n     */\n//    boolean isLeftMenuOpenNotEqual();\n\n    /**\n     * The menu is open on the right?\n     *\n     * @return true, otherwise false.\n     */\n//    boolean isRightMenuOpenNotEqual();\n\n    /**\n     * Open the current menu.\n     */\n//    void smoothOpenMenu();\n\n    /**\n     * Open the menu on left.\n     */\n//    void smoothOpenLeftMenu();\n\n    /**\n     * Open the menu on right.\n     */\n//    void smoothOpenRightMenu();\n\n    /**\n     * Open the menu on left for the duration.\n     *\n     * @param duration duration time.\n     */\n//    void smoothOpenLeftMenu(int duration);\n\n    /**\n     * Open the menu on right for the duration.\n     *\n     * @param duration duration time.\n     */\n//    void smoothOpenRightMenu(int duration);\n\n    // ---------- closeMenu. ---------- //\n\n    /**\n     * Smooth closed the menu.\n     */\n    void smoothCloseMenu();\n\n    /**\n     * Smooth closed the menu on the left.\n     */\n//    void smoothCloseLeftMenu();\n\n    /**\n     * Smooth closed the menu on the right.\n     */\n//    void smoothCloseRightMenu();\n\n    /**\n     * Smooth closed the menu for the duration.\n     *\n     * @param duration duration time.\n     */\n//    void smoothCloseMenu(int duration);\n\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/ExpandableAdapter.java",
    "content": "/*\n * Copyright 2019 Zhenjie Yan\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview;\n\nimport android.util.SparseBooleanArray;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.StaggeredGridLayoutManager;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Created by Zhenjie Yan on 1/28/19.\n */\npublic abstract class ExpandableAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {\n    private static final int TYPE_PARENT = 10000000;\n    private static final int TYPE_CHILD = 20000000;\n\n    private final SparseBooleanArray mExpandItemArray = new SparseBooleanArray();\n    private final List<Integer> mParentViewType = new ArrayList<>();\n\n    /**\n     * Parent item is expanded.\n     *\n     * @param parentPosition position of parent item.\n     *\n     * @return true, otherwise is false.\n     */\n    public final boolean isExpanded(int parentPosition) {\n        return mExpandItemArray.get(parentPosition, false);\n    }\n\n    /**\n     * Expand parent.\n     * 展开一个组\n     *\n     * @param parentPosition position of parent item.\n     */\n    public final void expandParent(int parentPosition) {\n        expandParent( parentPosition,false);\n    }\n    /**\n     * 展开一个组。\n     * 使用动画可能会造成闪屏或卡顿\n     *\n     * @param parentPosition\n     * @param animate\n     */\n    public void expandParent(int parentPosition, boolean animate) {\n        if (isExpanded(parentPosition)) {\n            return;\n        }\n        mExpandItemArray.append(parentPosition, true);\n        if (animate) {\n            int position = positionFromParentPosition(parentPosition);\n            int childCount = childItemCount(parentPosition);\n            notifyItemRangeInserted(position + 1, childCount);\n            notifyItemRangeChanged(position + 1 + childCount, getItemCount() - position - 1);\n        } else {\n            notifyDataSetChanged();\n        }\n    }\n\n    /**\n     * Collapse parent.\n     *\n     * @param parentPosition position of parent item.\n     */\n    public final void collapseParent(int parentPosition, boolean animate) {\n        if (!isExpanded(parentPosition)) {\n            return;\n        }\n        mExpandItemArray.append(parentPosition, false);\n        if (animate) {\n            int position = positionFromParentPosition(parentPosition);\n            int childCount = childItemCount(parentPosition);\n            notifyItemRangeRemoved(position + 1, childCount);\n            notifyItemRangeChanged(position + 1 + childCount, getItemCount() - position - 1);\n        }else {\n            notifyDataSetChanged();\n        }\n    }\n\n    public void collapseParent(int parentPosition) {\n        collapseParent(parentPosition,false);\n    }\n\n    /**\n     * Notify any registered observers that the item at <code>parentPosition</code> has changed.\n     *\n     * @param parentPosition position of parent item.\n     */\n    public final void notifyParentChanged(int parentPosition) {\n        int position = positionFromParentPosition(parentPosition);\n        notifyItemChanged(position);\n    }\n\n    /**\n     * Notify any registered observers that the item reflected at <code>parentPosition</code> has been newly inserted.\n     *\n     * @param parentPosition position of parent item.\n     */\n    public final void notifyParentInserted(int parentPosition) {\n        int position = positionFromParentPosition(parentPosition);\n        notifyItemInserted(position);\n    }\n\n    /**\n     * Notify any registered observers that the item previously located at <code>parentPosition</code> has been removed\n     * from the data set.\n     *\n     * @param parentPosition position of parent item.\n     */\n    public final void notifyParentRemoved(int parentPosition) {\n        int position = positionFromParentPosition(parentPosition);\n        notifyItemRemoved(position);\n    }\n\n    /**\n     * Notify any registered observers that the item at <code>parentPosition, childPosition</code> has changed.\n     *\n     * @param parentPosition position of parent item.\n     * @param childPosition positoin of child item.\n     */\n    public final void notifyChildChanged(int parentPosition, int childPosition) {\n        int position = positionFromChildPosition(parentPosition, childPosition);\n        notifyItemChanged(position);\n    }\n\n    /**\n     * Notify any registered observers that the item reflected at <code>parentPosition, childPosition</code> has been\n     * newly inserted.\n     *\n     * @param parentPosition position of parent item.\n     * @param childPosition positoin of child item.\n     */\n    public final void notifyChildInserted(int parentPosition, int childPosition) {\n        int position = positionFromChildPosition(parentPosition, childPosition);\n        notifyItemInserted(position);\n    }\n\n    /**\n     * Notify any registered observers that the item previously located at <code>parentPosition, childPosition</code>\n     * has been removed from the data set.\n     *\n     * @param parentPosition position of parent item.\n     * @param childPosition positoin of child item.\n     */\n    public final void notifyChildRemoved(int parentPosition, int childPosition) {\n        int position = positionFromChildPosition(parentPosition, childPosition);\n        notifyItemRemoved(position);\n    }\n\n    public int positionFromParentPosition(int parentPosition) {\n        int itemCount = 0;\n\n        int parentCount = parentItemCount();\n        for (int i = 0; i < parentCount; i++) {\n            itemCount += 1;\n\n            if (parentPosition == i) {\n                return itemCount - 1;\n            } else {\n                if (isExpanded(i)) {\n                    itemCount += childItemCount(i);\n                }\n            }\n        }\n\n        throw new IllegalStateException(\"The parent position is invalid: \" + parentPosition);\n    }\n\n    public int positionFromChildPosition(int parentPosition, int childPosition) {\n        int itemCount = 0;\n\n        int parentCount = parentItemCount();\n        for (int i = 0; i < parentCount; i++) {\n            itemCount += 1;\n\n            if (parentPosition == i) {\n                int childCount = childItemCount(parentPosition);\n                if (childPosition < childCount) {\n                    itemCount += (childPosition + 1);\n                    return itemCount - 1;\n                }\n\n                throw new IllegalStateException(\"The child position is invalid: \" + childPosition);\n            } else {\n                if (isExpanded(i)) {\n                    itemCount += childItemCount(i);\n                } else {\n                    // itemCount += 1;\n                }\n            }\n        }\n\n        throw new IllegalStateException(\"The parent position is invalid: \" + parentPosition);\n    }\n\n    @Override\n    public final int getItemCount() {\n        int parentCount = parentItemCount();\n        for (int i = 0; i < parentCount; i++) {\n            if (isExpanded(i)) {\n                int childCount = childItemCount(i);\n                parentCount += childCount;\n            }\n        }\n        return parentCount;\n    }\n\n    /**\n     * Get the total number of items in the parent.\n     */\n    public abstract int parentItemCount();\n\n    /**\n     * Get the total number of child items under parent.\n     *\n     * @param parentPosition position of parent item.\n     */\n    public abstract int childItemCount(int parentPosition);\n\n    @Override\n    public final int getItemViewType(int position) {\n        if (isParentItem(position)) {\n            if (!mParentViewType.contains(TYPE_PARENT)) mParentViewType.add(TYPE_PARENT);\n            return TYPE_PARENT;\n        } else {\n            return TYPE_CHILD;\n        }\n    }\n\n    /**\n     * Item is a parent item.\n     *\n     * @param adapterPosition adapter position.\n     *\n     * @return true, otherwise is false.\n     */\n    public final boolean isParentItem(int adapterPosition) {\n        int itemCount = 0;\n\n        int parentCount = parentItemCount();\n        for (int i = 0; i < parentCount; i++) {\n            if (itemCount == adapterPosition) {\n                return true;\n            }\n\n            itemCount += 1;\n\n            if (isExpanded(i)) {\n                itemCount += childItemCount(i);\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * Get the position of the parent item from the adapter position.\n     *\n     * @param adapterPosition adapter position of item.\n     */\n    public final int parentItemPosition(int adapterPosition) {\n        int itemCount = 0;\n        for (int i = 0; i < parentItemCount(); i++) {\n            itemCount += 1;\n\n            if (isExpanded(i)) {\n                int childCount = childItemCount(i);\n                itemCount += childCount;\n            }\n            if (adapterPosition < itemCount) {\n                return i;\n            }\n        }\n        return -1;\n\n//        throw new IllegalStateException(\"The adapter position is not a parent type: \" + adapterPosition);\n    }\n\n    /**\n     * Get the position of the child item from the adapter position.\n     *\n     * @param childAdapterPosition adapter position of child item.\n     */\n    public final int childItemPosition(int childAdapterPosition) {\n        int itemCount = 0;\n\n        int parentCount = parentItemCount();\n        for (int i = 0; i < parentCount; i++) {\n            itemCount += 1;\n\n            if (isExpanded(i)) {\n                int childCount = childItemCount(i);\n                itemCount += childCount;\n\n                if (childAdapterPosition < itemCount) {\n                    return childCount - (itemCount - childAdapterPosition);\n                }\n            } else {\n                // itemCount += 1;\n            }\n        }\n        return -1;\n        //throw new IllegalStateException(\"The adapter position is invalid: \" + childAdapterPosition);\n    }\n\n    @NonNull\n    @Override\n    public final VH onCreateViewHolder(@NonNull ViewGroup root, int viewType) {\n        if (mParentViewType.contains(viewType)) return createParentHolder(root, viewType);\n        return createChildHolder(root, viewType);\n    }\n\n    /**\n     * Called when RecyclerView needs a new {@link ViewHolder} of the given type to represent an parent item.\n     *\n     * @param root the ViewGroup into which the new View will be added after it is bound to an adapter position.\n     * @param viewType The view type of the new View.\n     *\n     * @return a new {@link ViewHolder} that holds a View of the given view type.\n     */\n    public abstract VH createParentHolder(@NonNull ViewGroup root, int viewType);\n\n    /**\n     * Called when RecyclerView needs a new {@link ViewHolder} of the given type to represent an child item.\n     *\n     * @param root the ViewGroup into which the new View will be added after it is bound to an adapter position.\n     * @param viewType The view type of the new View.\n     *\n     * @return a new {@link ViewHolder} that holds a View of the given view type.\n     */\n    public abstract VH createChildHolder(@NonNull ViewGroup root, int viewType);\n\n    @Override\n    public final void onBindViewHolder(@NonNull VH holder, int position, @NonNull List<Object> payloads) {\n        int parentPosition = parentItemPosition(position);\n        if (isParentItem(position)) {\n            bindParentHolder(holder, parentPosition, payloads);\n        } else {\n            int childPosition = childItemPosition(position);\n            bindChildHolder(holder, parentPosition, childPosition, payloads);\n        }\n    }\n\n    public void bindParentHolder(@NonNull VH holder, int position, @NonNull List<Object> payloads) {\n        bindParentHolder(holder, position);\n    }\n\n    public void bindChildHolder(@NonNull VH holder, int parentPosition, int position, @NonNull List<Object> payloads) {\n        bindChildHolder(holder, parentPosition, position);\n    }\n\n    /**\n     * Called by {@link RecyclerView} to display the data at the specified position. This method should update the\n     * contents of the {@link ViewHolder#itemView} to reflect the item at the given position.\n     *\n     * @param holder parent holder.\n     * @param position position of parent item.\n     */\n    public abstract void bindParentHolder(@NonNull VH holder, int position);\n\n    /**\n     * Called by {@link RecyclerView} to display the data at the specified position. This method should update the *\n     * contents of the {@link ViewHolder#itemView} to reflect the item at the given position.\n     *\n     * @param holder child holder.\n     * @param parentPosition position of parent item.\n     * @param position position of child position.\n     */\n    public abstract void bindChildHolder(@NonNull VH holder, int parentPosition, int position);\n\n//    @Deprecated\n    @Override\n    public final void onBindViewHolder(@NonNull VH holder, int position) {\n        int parentPosition = parentItemPosition(position);\n        if (isParentItem(position)) {\n            bindParentHolder(holder, parentPosition);\n        } else {\n            int childPosition = childItemPosition(position);\n            bindChildHolder(holder, parentPosition, childPosition);\n        }\n    }\n\n    public static abstract class ViewHolder extends RecyclerView.ViewHolder {\n\n        private ExpandableAdapter mAdapter;\n\n        public ViewHolder(@NonNull View itemView, ExpandableAdapter adapter) {\n            super(itemView);\n            this.mAdapter = adapter;\n        }\n\n//        /**\n//         * Determine if the current viewholder is a parent item.\n//         *\n//         * @return true, otherwise is false.\n//         */\n//        public final boolean isParentItem() {\n//            return mAdapter.isParentItem(getAdapterPosition());\n//        }\n//\n//        /**\n//         * Get the position of parent item.\n//         */\n//        public final int parentItemPosition() {\n//            return mAdapter.parentItemPosition(getAdapterPosition());\n//        }\n\n//        /**\n//         * Get the position of child item.\n//         */\n//        public final int childItemPosition() {\n//            if (isParentItem()) throw new IllegalStateException(\"This item is not a child item.\");\n//            return mAdapter.childItemPosition(getAdapterPosition());\n//        }\n\n//        /**\n//         * Parent item is expanded.\n//         *\n//         * @return true, otherwise is false.\n//         */\n//        public final boolean isParentExpanded() {\n//            return mAdapter.isExpanded(parentItemPosition());\n//        }\n    }\n\n    @Override\n    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {\n        RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();\n        if (lm instanceof GridLayoutManager) {\n            final GridLayoutManager glm = (GridLayoutManager)lm;\n            final GridLayoutManager.SpanSizeLookup originLookup = glm.getSpanSizeLookup();\n\n            glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {\n                @Override\n                public int getSpanSize(int position) {\n                    if (isParentItem(position)) return glm.getSpanCount();\n                    if (originLookup != null) return originLookup.getSpanSize(position);\n                    return 1;\n                }\n            });\n        }\n    }\n\n    @Override\n    public void onViewAttachedToWindow(@NonNull VH holder) {\n        if (isParentItem(holder.getAdapterPosition())) {\n            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();\n            if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {\n                StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams)lp;\n                p.setFullSpan(true);\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/Horizontal.java",
    "content": "/*\n * Copyright 2016 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview;\n\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.OverScroller;\n\n/**\n * Created by Yan Zhenjie on 2016/7/22.\n */\nabstract class Horizontal {\n\n    private int direction;\n    private View menuView;\n    protected Checker mChecker;\n\n    public Horizontal(int direction, View menuView) {\n        this.direction = direction;\n        this.menuView = menuView;\n        mChecker = new Checker();\n    }\n\n    public boolean canSwipe() {\n        if (menuView instanceof ViewGroup) {\n            return ((ViewGroup)menuView).getChildCount() > 0;\n        }\n        return false;\n    }\n\n    public boolean isCompleteClose(int scrollX) {\n        int i = -getMenuView().getWidth() * getDirection();\n        return scrollX == 0 && i != 0;\n    }\n\n    public abstract boolean isMenuOpen(int scrollX);\n\n    public abstract boolean isMenuOpenNotEqual(int scrollX);\n\n    public abstract void autoOpenMenu(OverScroller scroller, int scrollX, int duration);\n\n    public abstract void autoCloseMenu(OverScroller scroller, int scrollX, int duration);\n\n    public abstract Checker checkXY(int x, int y);\n\n    public abstract boolean isClickOnContentView(int contentViewWidth, float x);\n\n    public int getDirection() {\n        return direction;\n    }\n\n    public View getMenuView() {\n        return menuView;\n    }\n\n    public int getMenuWidth() {\n        return menuView.getWidth();\n    }\n\n    public static final class Checker {\n\n        public int x;\n        public int y;\n        public boolean shouldResetSwipe;\n    }\n\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/LeftHorizontal.java",
    "content": "/*\n * Copyright 2016 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview;\n\nimport android.view.View;\nimport android.widget.OverScroller;\n\n/**\n * Created by Yan Zhenjie on 2016/7/22.\n */\nclass LeftHorizontal extends Horizontal {\n\n    public LeftHorizontal(View menuView) {\n        super(SwipeRecyclerView.LEFT_DIRECTION, menuView);\n    }\n\n    @Override\n    public boolean isMenuOpen(int scrollX) {\n        int i = -getMenuView().getWidth() * getDirection();\n        return scrollX <= i && i != 0;\n    }\n\n    @Override\n    public boolean isMenuOpenNotEqual(int scrollX) {\n        return scrollX < -getMenuView().getWidth() * getDirection();\n    }\n\n    @Override\n    public void autoOpenMenu(OverScroller scroller, int scrollX, int duration) {\n        scroller.startScroll(Math.abs(scrollX), 0, getMenuView().getWidth() - Math.abs(scrollX), 0, duration);\n    }\n\n    @Override\n    public void autoCloseMenu(OverScroller scroller, int scrollX, int duration) {\n        scroller.startScroll(-Math.abs(scrollX), 0, Math.abs(scrollX), 0, duration);\n    }\n\n    @Override\n    public Checker checkXY(int x, int y) {\n        mChecker.x = x;\n        mChecker.y = y;\n        mChecker.shouldResetSwipe = false;\n        if (mChecker.x == 0) {\n            mChecker.shouldResetSwipe = true;\n        }\n        if (mChecker.x >= 0) {\n            mChecker.x = 0;\n        }\n        if (mChecker.x <= -getMenuView().getWidth()) {\n            mChecker.x = -getMenuView().getWidth();\n        }\n        return mChecker;\n    }\n\n    @Override\n    public boolean isClickOnContentView(int contentViewWidth, float x) {\n        return x > getMenuView().getWidth();\n    }\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/OnItemClickListener.java",
    "content": "/*\n * Copyright 2017 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview;\n\nimport android.view.View;\n\n/**\n * Created by YanZhenjie on 2017/7/21.\n */\npublic interface OnItemClickListener {\n\n    /**\n     * @param view target view.\n     * @param adapterPosition position of item.\n     */\n    void onItemClick(View view, int adapterPosition);\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/OnItemLongClickListener.java",
    "content": "/*\n * Copyright 2017 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview;\n\nimport android.view.View;\n\n/**\n * Created by YanZhenjie on 2017/7/21.\n */\npublic interface OnItemLongClickListener {\n\n    /**\n     * @param view target view.\n     * @param adapterPosition position of item.\n     */\n    void onItemLongClick(View view, int adapterPosition);\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/OnItemMenuClickListener.java",
    "content": "/*\n * Copyright 2016 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview;\n\n/**\n * Created by Yan Zhenjie on 2016/7/26.\n */\npublic interface OnItemMenuClickListener {\n\n    /**\n     * @param menuBridge menu bridge.\n     * @param adapterPosition position of item.\n     */\n    void onItemClick(SwipeMenuBridge menuBridge, int adapterPosition);\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/OnItemSwipeListener.java",
    "content": "package com.yanzhenjie.recyclerview;\n\nimport android.view.View;\n\n/**\n * Created by Wizos on 2019/4/14.\n */\n\npublic interface OnItemSwipeListener {\n    void onClose(View swipeMenu,int direction,int adapterPosition);\n    void onCloseLeft(int adapterPosition);\n    void onCloseRight(int adapterPosition);\n}\n"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/RightHorizontal.java",
    "content": "/*\n * Copyright 2016 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview;\n\nimport android.view.View;\nimport android.widget.OverScroller;\n\n/**\n * Created by Yan Zhenjie on 2016/7/22.\n */\nclass RightHorizontal extends Horizontal {\n\n    public RightHorizontal(View menuView) {\n        super(SwipeRecyclerView.RIGHT_DIRECTION, menuView);\n    }\n\n    @Override\n    public boolean isMenuOpen(int scrollX) {\n        int i = -getMenuView().getWidth() * getDirection();\n        return scrollX >= i && i != 0;\n    }\n\n    @Override\n    public boolean isMenuOpenNotEqual(int scrollX) {\n        return scrollX > -getMenuView().getWidth() * getDirection();\n    }\n\n    @Override\n    public void autoOpenMenu(OverScroller scroller, int scrollX, int duration) {\n        scroller.startScroll(Math.abs(scrollX), 0, getMenuView().getWidth() - Math.abs(scrollX), 0, duration);\n    }\n\n    @Override\n    public void autoCloseMenu(OverScroller scroller, int scrollX, int duration) {\n        scroller.startScroll(-Math.abs(scrollX), 0, Math.abs(scrollX), 0, duration);\n    }\n\n    @Override\n    public Checker checkXY(int x, int y) {\n        mChecker.x = x;\n        mChecker.y = y;\n        mChecker.shouldResetSwipe = false;\n        if (mChecker.x == 0) {\n            mChecker.shouldResetSwipe = true;\n        }\n        if (mChecker.x < 0) {\n            mChecker.x = 0;\n        }\n        if (mChecker.x > getMenuView().getWidth()) {\n            mChecker.x = getMenuView().getWidth();\n        }\n        return mChecker;\n    }\n\n    @Override\n    public boolean isClickOnContentView(int contentViewWidth, float x) {\n        return x < (contentViewWidth - getMenuView().getWidth());\n    }\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/StickyCreator.java",
    "content": "//package com.yanzhenjie.recyclerview;\n//\n//import androidx.annotation.NonNull;\n//import androidx.recyclerview.widget.RecyclerView;\n//import android.view.View;\n//\n///**\n// * Created by Wizos on 2019/4/20.\n// */\n//\n//public interface StickyCreator<VH extends RecyclerView.ViewHolder> {\n//    int STICKY_HEADER_GONE = 0;\n//    int STICKY_HEADER_VISIBLE = 1;\n//    int STICKY_HEADER_PUSHED_UP = 2;\n//\n\n//    // View setStickyHeaderView(ViewGroup view);\n//    int getGroupCount();\n//    int getStickyHeaderState(int firstVisibleGroupPosition, int firstVisibleChildPosition);\n//    void onBindStickyHeader(View header, int groupPosition, int childPosition, int alpha);\n//    //void onStickyHeaderClick(RecyclerView parent, View stickyHeaderView, int stickyGroupPosition);\n//    int getGroupPosition(int adapterPosition);\n//    int getChildPosition(int adapterPosition);\n//    boolean isGroup(int adapterPosition);\n//}\n"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/StickyHeaderLayout.java",
    "content": "//package com.yanzhenjie.recyclerview;\n//\n//import android.content.Context;\n//import androidx.annotation.AttrRes;\n//import androidx.annotation.NonNull;\n//import androidx.annotation.Nullable;\n//import androidx.recyclerview.widget.GridLayoutManager;\n//import androidx.recyclerview.widget.LinearLayoutManager;\n//import androidx.recyclerview.widget.RecyclerView;\n//import androidx.recyclerview.widget.StaggeredGridLayoutManager;\n//\n//import android.util.AttributeSet;\n//import android.util.Log;\n//import android.util.SparseArray;\n//import android.view.View;\n//import android.view.ViewGroup;\n//import android.widget.FrameLayout;\n//\n//\n//import java.lang.reflect.Method;\n//\n///**\n// * Depiction:头部吸顶布局。只要用StickyHeaderLayout包裹{@link RecyclerView},\n// * 并且实现{@link StickyCreator },就可以实现列表头部吸顶功能。\n// * StickyHeaderLayout只能包裹RecyclerView，而且只能包裹一个RecyclerView。\n// * <p>\n// * Author:donkingliang  QQ:1043214265\n// * Dat:2017/11/14\n// */\n//public class StickyHeaderLayout extends FrameLayout {\n//    private Context mContext;\n//    private RecyclerView mRecyclerView;\n//\n//    //吸顶容器，用于承载吸顶布局。\n//    private FrameLayout mStickyLayout;\n//\n//    //保存吸顶布局的缓存池。它以列表组头的viewType为key,ViewHolder为value对吸顶布局进行保存和回收复用。\n//    private final SparseArray<RecyclerView.ViewHolder> mStickyViews = new SparseArray<>();\n//\n//    //用于在吸顶布局中保存viewType的key。\n//    private final int VIEW_TAG_TYPE = -101;\n//\n//    //用于在吸顶布局中保存ViewHolder的key。\n//    private final int VIEW_TAG_HOLDER = -102;\n//\n//    //记录当前吸顶的组。\n//    private int mCurrentStickyGroup = -1;\n//\n//    //是否吸顶。\n//    private boolean isSticky = true;\n//\n//    //是否已经注册了adapter刷新监听\n//    private boolean isRegisterDataObserver = false;\n//\n//    public StickyHeaderLayout(@NonNull Context context) {\n//        super(context);\n//        mContext = context;\n//    }\n//\n//    public StickyHeaderLayout(@NonNull Context context, @Nullable AttributeSet attrs) {\n//        super(context, attrs);\n//        mContext = context;\n//    }\n//\n//    public StickyHeaderLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {\n//        super(context, attrs, defStyleAttr);\n//        mContext = context;\n//    }\n//\n//    private boolean enable = false;\n//    private StickyCreator mAdapter;\n//    @Override\n//    public void addView(View child, int index, ViewGroup.LayoutParams params) {\n//        if (getChildCount() > 0 || !(child instanceof RecyclerView)) {\n//            //外界只能向StickyHeaderLayout添加一个RecyclerView,而且只能添加RecyclerView。\n//            throw new IllegalArgumentException(\"StickyHeaderLayout can host only one direct child --> RecyclerView\");\n//        }\n//        super.addView(child, index, params);\n//\n//\n//        mRecyclerView = (RecyclerView) child;\n//\n//        // addOnScrollListener();\n//        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {\n//            @Override\n//            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {\n//                // 在滚动的时候，需要不断的更新吸顶布局。\n//                Log.e(\"滚动\",\"\" + isSticky);\n//                updateStickyView();\n//            }\n//        });\n//\n//        addStickyLayout();\n//    }\n//\n//    /**\n//     * 添加滚动监听\n//     */\n//    private void addOnScrollListener() {\n//        Log.e(\"吸附顶部A\",\"\" + isSticky);\n//        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {\n//            @Override\n//            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {\n//                // 在滚动的时候，需要不断的更新吸顶布局。\n//                Log.e(\"滚动\",\"\" + isSticky);\n//                updateStickyView();\n//            }\n//        });\n//    }\n//\n//    /**\n//     * 添加吸顶容器\n//     */\n//    private void addStickyLayout() {\n//        mStickyLayout = new FrameLayout(mContext);\n//        LayoutParams lp = new LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);\n//        mStickyLayout.setLayoutParams(lp);\n//        super.addView(mStickyLayout, 1, lp);\n//    }\n//\n//\n//\n//    public void setStickyHeaderView(View mHeaderView){\n//        mStickyHeaderView = mHeaderView;\n//        mStickyLayout.addView(mHeaderView);\n//    }\n//    /**\n//     * 用于在列表头显示的 View,mHeaderViewVisible 为 true 才可见\n//     */\n//    private View mStickyHeaderView;\n//    /**\n//     * 列表头是否可见\n//     */\n//    private boolean mHeaderViewVisible;\n//    private static final int MAX_ALPHA = 255;\n//    @Override\n//    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n//        if (mStickyHeaderView != null) {\n//            measureChild(mStickyHeaderView, widthMeasureSpec, heightMeasureSpec);\n//        }\n//        // Log.e(\"粘连布局：\",\"宽：\" + mHeaderViewWidth + \" , 高：\" + mHeaderViewHeight);\n//    }\n//\n//    private int mOldState = -1;\n//    @Override\n//    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n//        super.onLayout(changed, left, top, right, bottom);\n//        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();\n//\n//        // Log.e(\"粘连布局：\", \"onLayoutA: \" + adapter + \" , \" + mStickyHeaderView + \" , \" + mHeaderViewHeight + \" , \" + mHeaderViewWidth + \" , \" + mHeaderViewVisible);\n//        if ( adapter == null || !(adapter instanceof AdapterWrapper) ) {\n//            return;\n//        }\n//        StickyCreator mAdapter = (StickyCreator) ((AdapterWrapper)adapter).getOriginAdapter();\n//        if (mStickyHeaderView == null || mAdapter.getGroupCount() == 0) {\n//            return;\n//        }\n//        int firstVisibleItem = getFirstVisibleItem();\n//        //通过显示的第一个项的position获取它所在的组。\n//        int groupPos = mAdapter.getGroupPosition(firstVisibleItem);\n//        int childPos = mAdapter.getChildPosition(firstVisibleItem);\n//        int state = mAdapter.getStickyHeaderState(groupPos, childPos);\n//        Log.e(\"粘连布局：\", \"onLayoutB: \" + groupPos + \" , \" + childPos + \" , \" + state + \" , \"  + \" , \" + mHeaderViewVisible);\n//        if (state != mOldState) {\n//            mOldState = state;\n//        }\n//        // updateStickyView();\n//    }\n//\n//    private int lastVisibleItem = -1;\n//    public void updateStickyView() {\n//        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();\n//        if ( adapter == null || !(adapter instanceof AdapterWrapper) ) {\n//            return;\n//        }\n//        mAdapter = (StickyCreator) ((AdapterWrapper)adapter).getOriginAdapter();\n//        if (mStickyHeaderView == null || mAdapter.getGroupCount() == 0) {\n//            return;\n//        }\n//        registerAdapterDataObserver(adapter);\n//\n//\n//        //获取列表显示的第一个项。\n//        int firstVisibleItem = getFirstVisibleItem();\n//        //通过显示的第一个项的position获取它所在的组。\n//        int groupPosition = mAdapter.getGroupPosition(firstVisibleItem);\n//        int childPosition;\n//        if( mAdapter.isGroup(firstVisibleItem) ){\n//            childPosition = -1;\n//        }else {\n//            childPosition = mAdapter.getChildPosition(firstVisibleItem);\n//        }\n//        Log.e(\"检测\", \"   查\" + firstVisibleItem + \" ,\" + groupPosition + \" , \" + childPosition);\n//\n//        lastVisibleItem = firstVisibleItem;\n//\n//        int state = mAdapter.getStickyHeaderState(groupPosition, childPosition);\n//\n//        switch (state) {\n//            case StickyCreator.STICKY_HEADER_GONE: {\n//                mHeaderViewVisible = false;\n//                mStickyHeaderView.setVisibility(INVISIBLE);\n//                //Log.e(\"粘连布局：\", \"忽略: \"  + firstVisibleItem + \" [\"+ groupPosition + \",\" + childPosition + \"] , \" + mOldState + \" , [\" + mStickyHeaderView.getWidth() + \",\" + mStickyHeaderView.getHeight() + \"]\" );\n//                break;\n//            }\n//            case StickyCreator.STICKY_HEADER_VISIBLE: {\n//                mHeaderViewVisible = true;\n//                mStickyHeaderView.setVisibility(VISIBLE);\n//                mAdapter.onBindStickyHeader(mStickyHeaderView, groupPosition, childPosition, MAX_ALPHA);\n//                //Log.e(\"粘连布局：\", \"可见: \"  + firstVisibleItem + \" [\"+ groupPosition + \",\" + childPosition + \"] , \" + mOldState + \" , [\" + mStickyHeaderView.getWidth() + \",\" + mStickyHeaderView.getHeight() + \"]   \"  + MAX_ALPHA + \"  \" + mStickyHeaderView.getTop());\n//                //Log.e(\"粘连布局：\", \"池 \" + mOldState + \" , [\" + mStickyHeaderView.getWidth() + \",\" + mStickyHeaderView.getHeight() + \"]   \" + \"  \" + mStickyHeaderView.getTop() + \",\"+ mStickyHeaderView.getRight() + \",\"+ mStickyHeaderView.getBottom());\n//                mStickyHeaderView.setTranslationY(0);\n//                requestLayout();\n//                break;\n//            }\n//            case StickyCreator.STICKY_HEADER_PUSHED_UP: {\n//                mHeaderViewVisible = true;\n//                View firstView = mRecyclerView.getChildAt(0);\n//\n//                int bottom = firstView.getBottom();\n//                int headerHeight = mStickyHeaderView.getHeight();\n//                int y;\n//                int alpha;\n//                if (bottom < headerHeight) {\n//                    y = (bottom - headerHeight);\n//                    alpha = MAX_ALPHA * (headerHeight + y) / headerHeight;\n//                } else {\n//                    y = 0;\n//                    alpha = MAX_ALPHA;\n//                }\n//                mAdapter.onBindStickyHeader(mStickyHeaderView, groupPosition, childPosition, alpha);\n//                mStickyHeaderView.setTranslationY(y);\n//\n//                //Log.e(\"粘连布局：\", \"推动: \"  + firstVisibleItem + \" [\"+ groupPosition + \",\" + childPosition + \"] , \" + mOldState + \" , [\" + mStickyHeaderView.getWidth() + \",\" + mStickyHeaderView.getHeight() + \"]   \"  + MAX_ALPHA + \"  \" + mStickyHeaderView.getTop());\n//                //Log.e(\"粘连布局：\", \"   ------底部：\"  + bottom + \"，header高度：\"+ headerHeight + \"，顶部位置：\" + mStickyHeaderView.getTop() + \"，距离：\" + y );\n//                break;\n//            }\n//\n//        }\n//    }\n//\n//    /**\n//     * 注册adapter刷新监听\n//     */\n//    private void registerAdapterDataObserver(RecyclerView.Adapter adapter) {\n//        if (!isRegisterDataObserver) {\n//            isRegisterDataObserver = true;\n//            adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {\n//                @Override\n//                public void onChanged() {\n//                    updateStickyViewDelayed();\n//                }\n//\n//                @Override\n//                public void onItemRangeChanged(int positionStart, int itemCount) {\n//                    updateStickyViewDelayed();\n//                }\n//\n//                @Override\n//                public void onItemRangeInserted(int positionStart, int itemCount) {\n//                    updateStickyViewDelayed();\n//                }\n//\n//                @Override\n//                public void onItemRangeRemoved(int positionStart, int itemCount) {\n//                    updateStickyViewDelayed();\n//                }\n//            });\n//        }\n//    }\n//\n//    private void updateStickyViewDelayed() {\n//        postDelayed(new Runnable() {\n//            @Override\n//            public void run() {\n//                updateStickyView();\n//            }\n//        }, 100);\n//    }\n//\n////    /**\n////     * 判断是否需要先回收吸顶布局，如果要回收，则回收吸顶布局并返回null。\n////     * 如果不回收，则返回吸顶布局的ViewHolder。\n////     * 这样做可以避免频繁的添加和移除吸顶布局。\n////     *\n////     * @param viewType\n////     * @return\n////     */\n////    private RecyclerView.ViewHolder recycleStickyView(int viewType) {\n////        if (mStickyLayout.getChildCount() > 0) {\n////            View view = mStickyLayout.getChildAt(0);\n////            int type = (int) view.getTag(VIEW_TAG_TYPE);\n////            if (type == viewType) {\n////                return (RecyclerView.ViewHolder)view.getTag(VIEW_TAG_HOLDER);\n////            } else {\n////                recycle();\n////            }\n////        }\n////        return null;\n////    }\n////\n////    /**\n////     * 回收并移除吸顶布局\n////     */\n////    private void recycle() {\n////        if (mStickyLayout.getChildCount() > 0) {\n////            View view = mStickyLayout.getChildAt(0);\n////            mStickyViews.put((int) (view.getTag(VIEW_TAG_TYPE)), (RecyclerView.ViewHolder)view.getTag(VIEW_TAG_HOLDER));\n////            mStickyLayout.removeAllViews();\n////        }\n////    }\n//\n//    /**\n//     * 获取当前第一个显示的item .\n//     */\n//    private int getFirstVisibleItem() {\n//        int firstVisibleItem = -1;\n//        RecyclerView.LayoutManager layout = mRecyclerView.getLayoutManager();\n//        if (layout != null) {\n//            if (layout instanceof GridLayoutManager) {\n//                firstVisibleItem = ((GridLayoutManager) layout).findFirstVisibleItemPosition();\n//            } else if (layout instanceof LinearLayoutManager) {\n//                firstVisibleItem = ((LinearLayoutManager) layout).findFirstVisibleItemPosition();\n//            } else if (layout instanceof StaggeredGridLayoutManager) {\n//                int[] firstPositions = new int[((StaggeredGridLayoutManager) layout).getSpanCount()];\n//                ((StaggeredGridLayoutManager) layout).findFirstVisibleItemPositions(firstPositions);\n//                firstVisibleItem = getMin(firstPositions);\n//            }\n//        }\n//\n//        return firstVisibleItem;\n//    }\n//\n//    private int getMin(int[] arr) {\n//        int min = arr[0];\n//        for (int x = 1; x < arr.length; x++) {\n//            if (arr[x] < min)\n//                min = arr[x];\n//        }\n//        return min;\n//    }\n//\n//    @Override\n//    protected int computeVerticalScrollOffset() {\n//        if (mRecyclerView != null) {\n//            try {\n//                Method method = View.class.getDeclaredMethod(\"computeVerticalScrollOffset\");\n//                method.setAccessible(true);\n//                return (int) method.invoke(mRecyclerView);\n//            } catch (Exception e) {\n//                e.printStackTrace();\n//            }\n//        }\n//        return super.computeVerticalScrollOffset();\n//    }\n//\n//\n//    @Override\n//    protected int computeVerticalScrollRange() {\n//        if (mRecyclerView != null) {\n//            try {\n//                Method method = View.class.getDeclaredMethod(\"computeVerticalScrollRange\");\n//                method.setAccessible(true);\n//                return (int) method.invoke(mRecyclerView);\n//            } catch (Exception e) {\n//                e.printStackTrace();\n//            }\n//        }\n//        return super.computeVerticalScrollRange();\n//    }\n//\n//    @Override\n//    protected int computeVerticalScrollExtent() {\n//        if (mRecyclerView != null) {\n//            try {\n//                Method method = View.class.getDeclaredMethod(\"computeVerticalScrollExtent\");\n//                method.setAccessible(true);\n//                return (int) method.invoke(mRecyclerView);\n//            } catch (Exception e) {\n//                e.printStackTrace();\n//            }\n//        }\n//        return super.computeVerticalScrollExtent();\n//    }\n//\n//    @Override\n//    public void scrollBy(int x, int y) {\n//        if (mRecyclerView != null) {\n//            mRecyclerView.scrollBy(x,y);\n//        } else {\n//            super.scrollBy(x, y);\n//        }\n//    }\n//\n//    @Override\n//    public void scrollTo(int x, int y) {\n//        if (mRecyclerView != null) {\n//            mRecyclerView.scrollTo(x,y);\n//        } else {\n//            super.scrollTo(x, y);\n//        }\n//    }\n//}\n"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/StickyViewHolder.java",
    "content": "package com.yanzhenjie.recyclerview;\n\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * Created by Wizos on 2019/4/20.\n */\n\npublic class StickyViewHolder extends RecyclerView.ViewHolder  {\n\n    public StickyViewHolder(@NonNull View itemView) {\n        super(itemView);\n    }\n}\n"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/SwipeDragLayout.java",
    "content": "package com.yanzhenjie.recyclerview;\n\nimport android.content.Context;\nimport android.graphics.Point;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.widget.FrameLayout;\n\nimport androidx.core.view.ViewCompat;\nimport androidx.customview.widget.ViewDragHelper;\n\n/**\n * @author ditclear on 16/7/12. 可滑动的layout extends FrameLayout\n * https://github.com/ditclear/TimeLine/blob/master/swipelayout/src/main/java/com/ditclear/swipelayout/SwipeDragLayout.java\n * 实现主页的左右滑动已读未读\n */\npublic class SwipeDragLayout extends FrameLayout implements Controller{\n    private SwipeDragLayout mCacheView;\n    private View contentView;\n    private ViewDragHelper mDragHelper;\n    private Point originPos = new Point();\n    private int right = 0;\n    private int bottom = 0;\n    private boolean isOpen, ios = true;\n    // 偏移比率\n    private float offsetRatio;\n    //最小滑动距离的比例\n    private float needOffset; // 默认值为 0.45\n    private SwipeListener mListener;\n\n    private LeftHorizontal mSwipeLeftHorizontal;\n    private RightHorizontal mSwipeRightHorizontal;\n    private View leftMenuView;\n    private View rightMenuView;\n    public static final int DIRECTION_LEFT = 1;\n    public static final int DIRECTION_RIGHT = -1;\n\n    public SwipeDragLayout(Context context) {\n        this(context, null);\n    }\n\n    public SwipeDragLayout(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public SwipeDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        needOffset = 0.7f;\n        init();\n    }\n\n    // 初始化dragHelper，对拖动的view进行操作\n    private void init() {\n        // ViewDragHelper中拦截和处理事件时，需要会回调CallBack中的很多方法来决定一些事，比如：哪些子View可以移动、对个移动的View的边界的控制等等。\n        mDragHelper = ViewDragHelper.create(this, 0.32f, new ViewDragHelper.Callback() {\n            // 返回ture则表示可以捕获该view，你可以根据传入的第一个view参数决定哪些可以捕获\n            // 决定child是否可被拖拽。返回true则进行拖拽。\n            @Override\n            public boolean tryCaptureView(View child, int pointerId) {\n                return child == contentView;\n            }\n\n            /**\n             * 用来修正或者指定子View在水平方向上的移动\n             * clampViewPositionHorizontal 可以在该方法中对child移动的边界进行控制，left 分别为即将移动到的位置。\n             * 比如横向的情况下，我希望只在ViewGroup的内部移动，即：最小>=paddingleft，最大<=ViewGroup.getWidth()-paddingright-child.getWidth。\n             * @param child 被拖动的view\n             * @param left  是ViewDragHelper帮你计算好的View最新的left的值，left=view.getLeft()+dx\n             * @param dx   本次水平移动的距离\n             * @return  返回的值表示我们真正想让View的left变成的值\n             */\n            @Override\n            public int clampViewPositionHorizontal(View child, int left, int dx) {\n                //Log.e(\"移动\", \" 距离：\" + left + \"  ,  \"  +  dx );\n                return left;\n            }\n\n            @Override\n            public int clampViewPositionVertical(View child, int top, int dy) {\n                //fix issue 6 List滚动和Item左右滑动有冲突\n                // {@link https://github.com/ditclear/SwipeLayout/issues/6}\n                // getParent().requestDisallowInterceptTouchEvent(true);\n                return super.clampViewPositionVertical(child, top, dy);\n            }\n\n            // 要返回一个大于0的数，才会在在水平方向上对触摸到的View进行拖动。\n            // 方法的返回值应当是该childView横向或者纵向的移动的范围，当前如果只需要一个方向移动，可以只复写一个。\n            // 方法名为获取水平方向拖拽的范围，然而目前并没有用，该方法的返回值用来作为判断滑动方向的条件之一， 如果你想水平移动，那么该方法的返回值最好大于0\n            @Override\n            public int getViewHorizontalDragRange(View child) {\n                super.getViewHorizontalDragRange(child);\n                return 1;\n            }\n            /**\n             * 当View移动的时候调用\n             * @param changedView   当前移动的VIew\n             * @param left  当前View移动之后最新的left(应该是相对初始点的偏移量)\n             * @param top   当前View移动之后最新的top(应该是相对初始点的偏移量)\n             * @param dx    距离上次调用onViewPositionChanged，left变化的值，即水平移动的距离\n             * @param dy    垂直移动的距离\n             */\n            @Override\n            public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {\n                // 我们需要在x!=0的时候就行拦截。\n//                if (dx != 0) {\n//                    getParent().requestDisallowInterceptTouchEvent(true);\n//                }\n\n                final int childWidth = mSwipeRightHorizontal.getMenuWidth();\n                offsetRatio = -(float) (left - getPaddingLeft()) / childWidth;\n                //Log.e(\"侧滑\", \"滑动：\" + left + \"   \" + top  + \"   \" + dx  + \"   \" + dy  + \"  ==== \" + offsetRatio + \" , \" + getPaddingLeft() + \" , \" + childWidth );\n                //offsetRatio can callback here\n//                if (mListener!=null){\n//                    mListener.onUpdate(SwipeDragLayout.this, offsetRatio,left);\n//                }\n                // 兼容老版本\n                 invalidate();\n            }\n\n            //手指释放的时候回调\n            @Override\n            public void onViewReleased(View releasedChild, float xvel, float yvel) {\n//                Log.e(\"侧滑\", \"释放：\" + xvel + \"   \" + yvel );\n                // Note: needOffset 最小偏移量 应该由左/右菜单View 的宽度来定。（）\n                if (releasedChild == contentView) {\n                        if (offsetRatio != 0 && offsetRatio < needOffset) {\n                            if( Math.abs(offsetRatio) < needOffset ){\n                                close();\n                            }else {\n                                openLeft();\n                            }\n//                             Log.e(\"侧滑\",\"D：\" + offsetRatio + \"  \" + needOffset);\n                        } else if (offsetRatio == 0) {\n                            close();\n//                            Log.e(\"侧滑\",\"E：\" + offsetRatio + \"  \" + needOffset);\n                        } else {\n                            openRight();\n//                            Log.e(\"侧滑\",\"F：\" + offsetRatio + \"  \" + isOpen + \"  \" + needOffset);\n                        }\n//                    }\n                    invalidate();\n                }\n            }\n        });\n    }\n\n\n    public void openRight() {\n        mCacheView = SwipeDragLayout.this;\n        // Log.e(\"\",\"打开右侧\"  + isOpen + mListener );\n//        Log.d(\"Released and isOpen\", \"\" + isOpen);\n        closeRight();\n    }\n\n    public void openLeft() {\n        mCacheView = SwipeDragLayout.this;\n        // Log.e(\"侧滑\",\"打开左侧\"  + isOpen + mListener );\n        closeLeft();\n    }\n\n\n    private void smoothClose(boolean smooth) {\n        if (smooth) {\n            mDragHelper.smoothSlideViewTo(contentView, getPaddingLeft(), getPaddingTop());\n            // TODO: 2019/4/22 测试\n            ViewCompat.postInvalidateOnAnimation(this);\n//            postInvalidate();\n        } else {\n            contentView.layout(originPos.x, originPos.y, right, bottom);\n        }\n        isOpen = false;\n        mCacheView = null;\n    }\n\n\n\n    public void close() {\n        mDragHelper.settleCapturedViewAt(originPos.x, originPos.y);\n        isOpen = false;\n        mCacheView = null;\n//        mListener.onClosed(SwipeDragLayout.this);\n    }\n\n    public void closeLeft() {\n        // Log.e(\"侧滑\" , \"关闭左边\" + contentView);\n        if (contentView == null){\n            return;\n        }\n\n        if (mListener != null) {\n            mListener.onCloseLeft();\n            mListener.onClose(leftMenuView,DIRECTION_LEFT);\n        }\n        try {\n            mDragHelper.settleCapturedViewAt(originPos.x, originPos.y);\n        }catch (Exception e){\n            e.printStackTrace();\n            Log.e(\"\",\"报错\");\n        }\n        isOpen = false;\n        mCacheView = null;\n    }\n    public void closeRight() {\n        // Log.e(\"侧滑\" , \"关闭右侧\" + contentView);\n        if (contentView == null){\n            return;\n        }\n\n        if (mListener != null) {\n            mListener.onCloseRight();\n            mListener.onClose(rightMenuView,DIRECTION_RIGHT);\n        }\n        try {\n            mDragHelper.settleCapturedViewAt(originPos.x, originPos.y);\n        }catch (Exception e){\n            e.printStackTrace();\n            Log.e(\"\",\"报错\");\n        }\n        isOpen = false;\n        mCacheView = null;\n    }\n\n    @Override\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n        return mDragHelper.shouldInterceptTouchEvent(ev);\n    }\n\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        mDragHelper.processTouchEvent(event);\n        return true;\n    }\n\n\n    @Override\n    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n        super.onLayout(changed, left, top, right, bottom);\n        if (contentView != null) {\n            originPos.x = contentView.getLeft();\n            originPos.y = contentView.getTop();\n            this.right = contentView.getRight();\n            this.bottom = contentView.getBottom();\n\n\n            int contentViewWidth = contentView.getMeasuredWidthAndState();\n            int contentViewHeight = contentView.getMeasuredHeightAndState();\n            LayoutParams lp = (LayoutParams)contentView.getLayoutParams();\n            int start1 = getPaddingLeft();\n            int top1 = getPaddingTop() + lp.topMargin;\n            contentView.layout(start1, top1, start1 + contentViewWidth, top1 + contentViewHeight);\n        }\n\n        if (mSwipeLeftHorizontal != null) {\n            View leftMenu = mSwipeLeftHorizontal.getMenuView();\n            int menuViewWidth = leftMenu.getMeasuredWidthAndState();\n            int menuViewHeight = leftMenu.getMeasuredHeightAndState();\n            LayoutParams lp = (LayoutParams)leftMenu.getLayoutParams();\n            int top1 = getPaddingTop() + lp.topMargin;\n//            leftMenu.layout(-menuViewWidth, top1, 0, top1 + menuViewHeight);\n            leftMenu.layout(0, top1, menuViewWidth, top1 + menuViewHeight);\n        }\n\n        if (mSwipeRightHorizontal != null) {\n            View rightMenu = mSwipeRightHorizontal.getMenuView();\n            int menuViewWidth = rightMenu.getMeasuredWidthAndState();\n            int menuViewHeight = rightMenu.getMeasuredHeightAndState();\n            LayoutParams lp = (LayoutParams)rightMenu.getLayoutParams();\n            int top1 = getPaddingTop() + lp.topMargin;\n\n            int parentViewWidth = getMeasuredWidthAndState();\n//            rightMenu.layout(parentViewWidth, top1, parentViewWidth + menuViewWidth, top1 + menuViewHeight);\n            rightMenu.layout(parentViewWidth - menuViewWidth, top1, parentViewWidth, top1 + menuViewHeight);\n        }\n    }\n\n    // 持续平滑动画 高频调用\n    @Override\n    public void computeScroll() {\n        // 如果返回true，动画还需要继续\n        if (mDragHelper.continueSettling(true)) {\n            ViewCompat.postInvalidateOnAnimation(this);\n            // invalidate();\n        }\n    }\n\n    // 当View中所有的子控件均被映射成xml后触发.当加载完成xml后，就会执行这个方法。\n    @Override\n    protected void onFinishInflate() {\n        super.onFinishInflate();\n        leftMenuView = findViewById(R.id.swipe_left);\n        rightMenuView = findViewById(R.id.swipe_right);\n        contentView = findViewById(R.id.swipe_content);\n        mSwipeLeftHorizontal = new LeftHorizontal(leftMenuView);\n        mSwipeRightHorizontal = new RightHorizontal(rightMenuView);\n    }\n\n    // onDetachedFromWindow方法是在Activity destroy的时候被调用的，也就是act对应的window被删除的时候，且每个view只会被调用一次，父view的调用在后，也不论view的visibility状态都会被调用，适合做最后的清理操作；\n    @Override\n    protected void onDetachedFromWindow() {\n        if (mCacheView == this) {\n            mCacheView.smoothClose(false);\n            mCacheView = null;\n        }\n        super.onDetachedFromWindow();\n    }\n\n\n    public void setListener(SwipeListener listener) {\n        mListener = listener;\n    }\n\n    //滑动监听\n    public interface SwipeListener {\n//        /**\n//         * 拖动中，可根据offset 进行其他动画\n//         * @param view\n//         * @param offset 偏移量\n//         */\n//        void onUpdate(View view, float offsetRatio, int offset);\n\n//        void onOpened(View view);\n\n//        void onClosed(View view);\n//        void onCloseLeft(View view);\n//        void onCloseRight(View view);\n\n        void onClose(View swipeMenu,int direction);\n        void onCloseLeft();\n        void onCloseRight();\n\n//        /**\n//         * 点击内容layout {@link #onFinishInflate()}\n//         * @param view\n//         */\n//        void onClick(View view);\n\n//        void log(String temp);\n    }\n\n\n//    private float mOpenPercent = 0.5f;\n//    /**\n//     * Set open percentage.\n//     *\n//     * @param openPercent such as 0.5F.\n//     */\n//    public void setOpenPercent(float openPercent) {\n//        this.mOpenPercent = openPercent;\n//    }\n//    /**\n//     * The duration of the set.\n//     *\n//     * @param scrollerDuration such as 500.\n//     */\n//    public void setScrollerDuration(int scrollerDuration) {\n//        this.mScrollerDuration = scrollerDuration;\n//    }\n\n    @Override\n    public boolean isMenuOpen() {\n        return isLeftMenuOpen() || isRightMenuOpen();\n    }\n\n    public boolean isLeftMenuOpen() {\n        return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.isMenuOpen(getScrollX());\n    }\n\n    public boolean isRightMenuOpen() {\n        return mSwipeRightHorizontal != null && mSwipeRightHorizontal.isMenuOpen(getScrollX());\n    }\n\n    @Override\n    public void smoothCloseMenu() {\n        close();\n    }\n\n\n    public boolean hasLeftMenu() {\n        return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.canSwipe();\n    }\n\n    public boolean hasRightMenu() {\n        return mSwipeRightHorizontal != null && mSwipeRightHorizontal.canSwipe();\n    }\n}\n"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/SwipeMenu.java",
    "content": "/*\n * Copyright 2016 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview;\n\nimport android.widget.LinearLayout;\n\nimport androidx.annotation.IntDef;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Created by Yan Zhenjie on 2016/7/22.\n */\npublic class SwipeMenu {\n\n    @IntDef({HORIZONTAL, VERTICAL})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface OrientationMode {}\n\n    public static final int HORIZONTAL = LinearLayout.HORIZONTAL;\n    public static final int VERTICAL = LinearLayout.VERTICAL;\n\n    private SwipeDragLayout mMenuLayout;\n    private int mOrientation;\n    private List<SwipeMenuItem> mSwipeMenuItems;\n\n    public SwipeMenu(SwipeDragLayout menuLayout) {\n        this.mMenuLayout = menuLayout;\n        this.mOrientation = SwipeMenu.HORIZONTAL;\n        this.mSwipeMenuItems = new ArrayList<>(2);\n    }\n\n//    /**\n//     * Set a percentage.\n//     *\n//     * @param openPercent such as 0.5F.\n//     */\n//    public void setOpenPercent(@FloatRange(from = 0.1, to = 1) float openPercent) {\n//        mMenuLayout.setOpenPercent(openPercent);\n//    }\n//\n//    /**\n//     * The duration of the set.\n//     *\n//     * @param scrollerDuration such 500.\n//     */\n//    public void setScrollerDuration(@IntRange(from = 1) int scrollerDuration) {\n//        mMenuLayout.setScrollerDuration(scrollerDuration);\n//    }\n\n    /**\n     * Set the menu mOrientation.\n     *\n     * @param orientation use {@link SwipeMenu#HORIZONTAL} or {@link SwipeMenu#VERTICAL}.\n     *\n     * @see SwipeMenu#HORIZONTAL\n     * @see SwipeMenu#VERTICAL\n     */\n    public void setOrientation(@OrientationMode int orientation) {\n        this.mOrientation = orientation;\n    }\n\n    /**\n     * Get the menu mOrientation.\n     *\n     * @return {@link SwipeMenu#HORIZONTAL} or {@link SwipeMenu#VERTICAL}.\n     */\n    @OrientationMode\n    public int getOrientation() {\n        return mOrientation;\n    }\n\n    public void addMenuItem(SwipeMenuItem item) {\n        mSwipeMenuItems.add(item);\n    }\n\n    public void removeMenuItem(SwipeMenuItem item) {\n        mSwipeMenuItems.remove(item);\n    }\n\n    public List<SwipeMenuItem> getMenuItems() {\n        return mSwipeMenuItems;\n    }\n\n    public boolean hasMenuItems() {\n        return !mSwipeMenuItems.isEmpty();\n    }\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/SwipeMenuBridge.java",
    "content": "/*\n * Copyright 2017 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview;\n\n/**\n * Created by YanZhenjie on 2017/7/20.\n */\npublic class SwipeMenuBridge {\n\n    private final Controller mController;\n    private final int mDirection;\n    private final int mPosition;\n    //private final View swipeMenu;\n\n    public SwipeMenuBridge(Controller controller, int direction, int position) {\n        this.mController = controller;\n        this.mDirection = direction;\n        this.mPosition = position;\n        //this.swipeMenu = swipeMenu;\n    }\n\n    //public View getSwipeMenu() {\n    //    return swipeMenu;\n    //}\n\n    @SwipeRecyclerView.DirectionMode\n    public int getDirection() {\n        return mDirection;\n    }\n\n    /**\n     * Get the position of button in the menu.\n     */\n    public int getPosition() {\n        return mPosition;\n    }\n\n    public void closeMenu() {\n        mController.smoothCloseMenu();\n    }\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/SwipeMenuCreator.java",
    "content": "/*\n * Copyright 2016 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview;\n\n/**\n * Created by Yan Zhenjie on 2016/7/26.\n */\npublic interface SwipeMenuCreator {\n\n    /**\n     * Create menu for recyclerVie item.\n     *\n     * @param leftMenu the menu on the left.\n     * @param rightMenu the menu on the right.\n     * @param position the position of item.\n     */\n    void onCreateMenu(SwipeMenu leftMenu, SwipeMenu rightMenu, int position);\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/SwipeMenuItem.java",
    "content": "/*\n * Copyright 2016 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview;\n\nimport android.content.Context;\nimport android.content.res.ColorStateList;\nimport android.graphics.Typeface;\nimport android.graphics.drawable.ColorDrawable;\nimport android.graphics.drawable.Drawable;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.ColorRes;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.StringRes;\nimport androidx.annotation.StyleRes;\nimport androidx.core.content.ContextCompat;\n\n/**\n * Created by Yan Zhenjie on 2016/7/26.\n */\npublic class SwipeMenuItem {\n\n    private Context mContext;\n    private Drawable background;\n    private Drawable icon;\n    private String title;\n    private ColorStateList titleColor;\n    private int titleSize;\n    private Typeface textTypeface;\n    private int textAppearance;\n    private int width = -2;\n    private int height = -2;\n    private int weight = 0;\n\n    public SwipeMenuItem(Context context) {\n        mContext = context;\n    }\n\n    public SwipeMenuItem setBackground(@DrawableRes int resId) {\n        return setBackground(ContextCompat.getDrawable(mContext, resId));\n    }\n\n    public SwipeMenuItem setBackground(Drawable background) {\n        this.background = background;\n        return this;\n    }\n\n    public SwipeMenuItem setBackgroundColorResource(@ColorRes int color) {\n        return setBackgroundColor(ContextCompat.getColor(mContext, color));\n    }\n\n    public SwipeMenuItem setBackgroundColor(@ColorInt int color) {\n        this.background = new ColorDrawable(color);\n        return this;\n    }\n\n    public Drawable getBackground() {\n        return background;\n    }\n\n    public SwipeMenuItem setImage(@DrawableRes int resId) {\n        return setImage(ContextCompat.getDrawable(mContext, resId));\n    }\n\n    public SwipeMenuItem setImage(Drawable icon) {\n        this.icon = icon;\n        return this;\n    }\n\n    public Drawable getImage() {\n        return icon;\n    }\n\n    public SwipeMenuItem setText(@StringRes int resId) {\n        return setText(mContext.getString(resId));\n    }\n\n    public SwipeMenuItem setText(String title) {\n        this.title = title;\n        return this;\n    }\n\n    public String getText() {\n        return title;\n    }\n\n    public SwipeMenuItem setTextColorResource(@ColorRes int titleColor) {\n        return setTextColor(ContextCompat.getColor(mContext, titleColor));\n    }\n\n    public SwipeMenuItem setTextColor(@ColorInt int titleColor) {\n        this.titleColor = ColorStateList.valueOf(titleColor);\n        return this;\n    }\n\n    public ColorStateList getTitleColor() {\n        return titleColor;\n    }\n\n    public SwipeMenuItem setTextSize(int titleSize) {\n        this.titleSize = titleSize;\n        return this;\n    }\n\n    public int getTextSize() {\n        return titleSize;\n    }\n\n    public SwipeMenuItem setTextAppearance(@StyleRes int textAppearance) {\n        this.textAppearance = textAppearance;\n        return this;\n    }\n\n    public int getTextAppearance() {\n        return textAppearance;\n    }\n\n    public SwipeMenuItem setTextTypeface(Typeface textTypeface) {\n        this.textTypeface = textTypeface;\n        return this;\n    }\n\n    public Typeface getTextTypeface() {\n        return textTypeface;\n    }\n\n    public SwipeMenuItem setWidth(int width) {\n        this.width = width;\n        return this;\n    }\n\n    public int getWidth() {\n        return width;\n    }\n\n    public SwipeMenuItem setHeight(int height) {\n        this.height = height;\n        return this;\n    }\n\n    public int getHeight() {\n        return height;\n    }\n\n    public SwipeMenuItem setWeight(int weight) {\n        this.weight = weight;\n        return this;\n    }\n    public int leftMargin = 0;\n    public int topMargin = 0;\n    public int rightMargin = 0;\n    public int bottomMargin = 0;\n    public SwipeMenuItem setMargins(int left, int top, int right, int bottom) {\n        leftMargin = left;\n        topMargin = top;\n        rightMargin = right;\n        bottomMargin = bottom;\n        return this;\n    }\n\n    public int getLeftMargin() {\n        return leftMargin;\n    }\n\n    public int getTopMargin() {\n        return topMargin;\n    }\n\n    public int getRightMargin() {\n        return rightMargin;\n    }\n\n    public int getBottomMargin() {\n        return bottomMargin;\n    }\n\n    public int getWeight() {\n        return weight;\n    }\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/SwipeMenuLayout.java",
    "content": "///*\n// * Copyright 2016 Yan Zhenjie\n// *\n// * Licensed under the Apache License, Version 2.0 (the \"License\");\n// * you may not use this file except in compliance with the License.\n// * You may obtain a copy of the License at\n// *\n// *      http://www.apache.org/licenses/LICENSE-2.0\n// *\n// * Unless required by applicable law or agreed to in writing, software\n// * distributed under the License is distributed on an \"AS IS\" BASIS,\n// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// * See the License for the specific language governing permissions and\n// * limitations under the License.\n// */\n//package com.yanzhenjie.recyclerview;\n//\n//import android.content.Context;\n//import android.content.res.TypedArray;\n//import android.support.v4.view.ViewCompat;\n//import android.util.AttributeSet;\n//import android.view.Gravity;\n//import android.view.MotionEvent;\n//import android.view.VelocityTracker;\n//import android.view.View;\n//import android.view.ViewConfiguration;\n//import android.widget.FrameLayout;\n//import android.widget.OverScroller;\n//import android.widget.TextView;\n//\n///**\n// * Created by Yan Zhenjie on 2016/7/27.\n// */\n//public class SwipeMenuLayout extends FrameLayout implements Controller {\n//\n//    public static final int DEFAULT_SCROLLER_DURATION = 200;\n//\n//    private int mLeftViewId = 0;\n//    private int mContentViewId = 0;\n//    private int mRightViewId = 0;\n//\n//    private float mOpenPercent = 0.5f;\n//    private int mScrollerDuration = DEFAULT_SCROLLER_DURATION;\n//\n//    private int mScaledTouchSlop;\n//    private int mLastX;\n//    private int mLastY;\n//    private int mDownX;\n//    private int mDownY;\n//    private View mContentView;\n//    private LeftHorizontal mSwipeLeftHorizontal;\n//    private RightHorizontal mSwipeRightHorizontal;\n//    private Horizontal mSwipeCurrentHorizontal;\n//    private boolean shouldResetSwipe;\n//    private boolean mDragging;\n//    private boolean swipeEnable = true;\n//    private OverScroller mScroller;\n//    private VelocityTracker mVelocityTracker;\n//    private int mScaledMinimumFlingVelocity;\n//    private int mScaledMaximumFlingVelocity;\n//\n//\n//    public SwipeMenuLayout(Context context) {\n//        this(context, null);\n//    }\n//\n//    public SwipeMenuLayout(Context context, AttributeSet attrs) {\n//        this(context, attrs, 0);\n//    }\n//\n//    public SwipeMenuLayout(Context context, AttributeSet attrs, int defStyle) {\n//        super(context, attrs, defStyle);\n//        setClickable(true);\n//\n//        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SwipeMenuLayout);\n//        mLeftViewId = typedArray.getResourceId(R.styleable.SwipeMenuLayout_leftViewId, mLeftViewId);\n//        mContentViewId = typedArray.getResourceId(R.styleable.SwipeMenuLayout_contentViewId, mContentViewId);\n//        mRightViewId = typedArray.getResourceId(R.styleable.SwipeMenuLayout_rightViewId, mRightViewId);\n//        typedArray.recycle();\n//\n//        ViewConfiguration configuration = ViewConfiguration.get(getContext());\n//        mScaledTouchSlop = configuration.getScaledTouchSlop();\n//        mScaledMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();\n//        mScaledMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();\n//\n//        mScroller = new OverScroller(getContext());\n//    }\n//\n//    @Override\n//    protected void onFinishInflate() {\n//        super.onFinishInflate();\n//        if (mLeftViewId != 0 && mSwipeLeftHorizontal == null) {\n//            View view = findViewById(mLeftViewId);\n//            mSwipeLeftHorizontal = new LeftHorizontal(view);\n//        }\n//        if (mRightViewId != 0 && mSwipeRightHorizontal == null) {\n//            View view = findViewById(mRightViewId);\n//            mSwipeRightHorizontal = new RightHorizontal(view);\n//        }\n//        if (mContentViewId != 0 && mContentView == null) {\n//            mContentView = findViewById(mContentViewId);\n//        } else {\n//            TextView errorView = new TextView(getContext());\n//            errorView.setClickable(true);\n//            errorView.setGravity(Gravity.CENTER);\n//            errorView.setTextSize(16);\n//            errorView.setText(\"You may not have set the ContentView.\");\n//            mContentView = errorView;\n//            addView(mContentView);\n//        }\n//    }\n//\n//    /**\n//     * Set whether open swipe. Default is true.\n//     *\n//     * @param swipeEnable true open, otherwise false.\n//     */\n//    public void setSwipeEnable(boolean swipeEnable) {\n//        this.swipeEnable = swipeEnable;\n//    }\n//\n//    /**\n//     * Open the swipe function of the Item?\n//     *\n//     * @return open is true, otherwise is false.\n//     */\n//    public boolean isSwipeEnable() {\n//        return swipeEnable;\n//    }\n//\n//    /**\n//     * Set open percentage.\n//     *\n//     * @param openPercent such as 0.5F.\n//     */\n//    public void setOpenPercent(float openPercent) {\n//        this.mOpenPercent = openPercent;\n//    }\n//\n//    /**\n//     * Get open percentage.\n//     *\n//     * @return such as 0.5F.\n//     */\n//    public float getOpenPercent() {\n//        return mOpenPercent;\n//    }\n//\n//    /**\n//     * The duration of the set.\n//     *\n//     * @param scrollerDuration such as 500.\n//     */\n//    public void setScrollerDuration(int scrollerDuration) {\n//        this.mScrollerDuration = scrollerDuration;\n//    }\n//\n//    @Override\n//    public boolean onInterceptTouchEvent(MotionEvent ev) {\n//        boolean isIntercepted = super.onInterceptTouchEvent(ev);\n//        if (!isSwipeEnable()) {\n//            return isIntercepted;\n//        }\n//\n//        int action = ev.getAction();\n//        switch (action) {\n//            case MotionEvent.ACTION_DOWN: {\n//                mDownX = mLastX = (int)ev.getX();\n//                mDownY = (int)ev.getY();\n//                return false;\n//            }\n//            case MotionEvent.ACTION_MOVE: {\n//                int disX = (int)(ev.getX() - mDownX);\n//                int disY = (int)(ev.getY() - mDownY);\n//                return Math.abs(disX) - Math.abs(disY) > mScaledTouchSlop;\n////                return Math.abs(disX) > mScaledTouchSlop && Math.abs(disX) > Math.abs(disY);\n//            }\n//            case MotionEvent.ACTION_UP: {\n//                boolean isClick = mSwipeCurrentHorizontal != null &&\n//                    mSwipeCurrentHorizontal.isClickOnContentView(getWidth(), ev.getX());\n//                if (isMenuOpen() && isClick) {\n//                    smoothCloseMenu();\n//                    return true;\n//                }\n//                return false;\n//            }\n//            case MotionEvent.ACTION_CANCEL: {\n//                if (!mScroller.isFinished()) mScroller.abortAnimation();\n//                return false;\n//            }\n//        }\n//        return isIntercepted;\n//    }\n//\n//    @Override\n//    public boolean onTouchEvent(MotionEvent ev) {\n//        if (!isSwipeEnable()) {\n//            return super.onTouchEvent(ev);\n//        }\n//\n//        if (mVelocityTracker == null) mVelocityTracker = VelocityTracker.obtain();\n//        mVelocityTracker.addMovement(ev);\n//        int dx;\n//        int dy;\n//        int action = ev.getAction();\n//        switch (action) {\n//            case MotionEvent.ACTION_DOWN: {\n//                mLastX = (int)ev.getX();\n//                mLastY = (int)ev.getY();\n//                break;\n//            }\n//            case MotionEvent.ACTION_MOVE: {\n//                int disX = (int)(mLastX - ev.getX());\n//                int disY = (int)(mLastY - ev.getY());\n//                if (!mDragging && Math.abs(disX) > mScaledTouchSlop && Math.abs(disX) > Math.abs(disY)) {\n//                    mDragging = true;\n//                }\n//                if (mDragging) {\n//                    if (mSwipeCurrentHorizontal == null || shouldResetSwipe) {\n//                        if (disX < 0) {\n//                            if (mSwipeLeftHorizontal != null) {\n//                                mSwipeCurrentHorizontal = mSwipeLeftHorizontal;\n//                            } else {\n//                                mSwipeCurrentHorizontal = mSwipeRightHorizontal;\n//                            }\n//                        } else {\n//                            if (mSwipeRightHorizontal != null) {\n//                                mSwipeCurrentHorizontal = mSwipeRightHorizontal;\n//                            } else {\n//                                mSwipeCurrentHorizontal = mSwipeLeftHorizontal;\n//                            }\n//                        }\n//                    }\n//                    scrollBy(disX, 0);\n//                    mLastX = (int)ev.getX();\n//                    mLastY = (int)ev.getY();\n//                    shouldResetSwipe = false;\n//                }\n//                break;\n//            }\n//            case MotionEvent.ACTION_UP: {\n//                dx = (int)(mDownX - ev.getX());\n//                dy = (int)(mDownY - ev.getY());\n//                mDragging = false;\n//                mVelocityTracker.computeCurrentVelocity(1000, mScaledMaximumFlingVelocity);\n//                int velocityX = (int)mVelocityTracker.getXVelocity();\n//                int velocity = Math.abs(velocityX);\n//                if (velocity > mScaledMinimumFlingVelocity) {\n//                    if (mSwipeCurrentHorizontal != null) {\n//                        int duration = getSwipeDuration(ev, velocity);\n//                        if (mSwipeCurrentHorizontal instanceof RightHorizontal) {\n//                            if (velocityX < 0) {\n//                                smoothOpenMenu(duration);\n//                            } else {\n//                                smoothCloseMenu(duration);\n//                            }\n//                        } else {\n//                            if (velocityX > 0) {\n//                                smoothOpenMenu(duration);\n//                            } else {\n//                                smoothCloseMenu(duration);\n//                            }\n//                        }\n//                        ViewCompat.postInvalidateOnAnimation(this);\n//                    }\n//                } else {\n//                    judgeOpenClose(dx, dy);\n//                }\n//                mVelocityTracker.clear();\n//                mVelocityTracker.recycle();\n//                mVelocityTracker = null;\n//                if (Math.abs(mDownX - ev.getX()) > mScaledTouchSlop ||\n//                    Math.abs(mDownY - ev.getY()) > mScaledTouchSlop || isLeftMenuOpen() || isRightMenuOpen()) {\n//                    ev.setAction(MotionEvent.ACTION_CANCEL);\n//                    super.onTouchEvent(ev);\n//                    return true;\n//                }\n//                break;\n//            }\n//            case MotionEvent.ACTION_CANCEL: {\n//                mDragging = false;\n//                if (!mScroller.isFinished()) {\n//                    mScroller.abortAnimation();\n//                } else {\n//                    dx = (int)(mDownX - ev.getX());\n//                    dy = (int)(mDownY - ev.getY());\n//                    judgeOpenClose(dx, dy);\n//                }\n//                break;\n//            }\n//        }\n//        return super.onTouchEvent(ev);\n//    }\n//\n//    /**\n//     * compute finish duration.\n//     *\n//     * @param ev up event.\n//     * @param velocity velocity x.\n//     *\n//     * @return finish duration.\n//     */\n//    private int getSwipeDuration(MotionEvent ev, int velocity) {\n//        int sx = getScrollX();\n//        int dx = (int)(ev.getX() - sx);\n//        final int width = mSwipeCurrentHorizontal.getMenuWidth();\n//        final int halfWidth = width / 2;\n//        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);\n//        final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio);\n//        int duration;\n//        if (velocity > 0) {\n//            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));\n//        } else {\n//            final float pageDelta = (float)Math.abs(dx) / width;\n//            duration = (int)((pageDelta + 1) * 100);\n//        }\n//        duration = Math.min(duration, mScrollerDuration);\n//        return duration;\n//    }\n//\n//    float distanceInfluenceForSnapDuration(float f) {\n//        f -= 0.5f; // center the values about 0.\n//        f *= 0.3f * Math.PI / 2.0f;\n//        return (float)Math.sin(f);\n//    }\n//\n//    private void judgeOpenClose(int dx, int dy) {\n//        if (mSwipeCurrentHorizontal != null) {\n//            if (Math.abs(getScrollX()) >=\n//                (mSwipeCurrentHorizontal.getMenuView().getWidth() * mOpenPercent)) { // auto open\n//                if (Math.abs(dx) > mScaledTouchSlop || Math.abs(dy) > mScaledTouchSlop) { // swipe up\n//                    if (isMenuOpenNotEqual()) {\n//                        smoothCloseMenu();\n//                    } else {\n//                        smoothOpenMenu();\n//                    }\n//                } else { // normal up\n//                    if (isMenuOpen()) {\n//                        smoothCloseMenu();\n//                    } else {\n//                        smoothOpenMenu();\n//                    }\n//                }\n//            } else { // auto closeMenu\n//                smoothCloseMenu();\n//            }\n//        }\n//    }\n//\n//    @Override\n//    public void scrollTo(int x, int y) {\n//        if (mSwipeCurrentHorizontal == null) {\n//            super.scrollTo(x, y);\n//        } else {\n//            Horizontal.Checker checker = mSwipeCurrentHorizontal.checkXY(x, y);\n//            shouldResetSwipe = checker.shouldResetSwipe;\n//            if (checker.x != getScrollX()) {\n//                super.scrollTo(checker.x, checker.y);\n//            }\n//        }\n//    }\n//\n//    @Override\n//    public void computeScroll() {\n//        if (mScroller.computeScrollOffset() && mSwipeCurrentHorizontal != null) {\n//            if (mSwipeCurrentHorizontal instanceof RightHorizontal) {\n//                scrollTo(Math.abs(mScroller.getCurrX()), 0);\n//                invalidate();\n//            } else {\n//                scrollTo(-Math.abs(mScroller.getCurrX()), 0);\n//                invalidate();\n//            }\n//        }\n//    }\n//\n//    public boolean hasLeftMenu() {\n//        return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.canSwipe();\n//    }\n//\n//    public boolean hasRightMenu() {\n//        return mSwipeRightHorizontal != null && mSwipeRightHorizontal.canSwipe();\n//    }\n//\n//    @Override\n//    public boolean isMenuOpen() {\n//        return isLeftMenuOpen() || isRightMenuOpen();\n//    }\n//\n//    @Override\n//    public boolean isLeftMenuOpen() {\n//        return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.isMenuOpen(getScrollX());\n//    }\n//\n//    @Override\n//    public boolean isRightMenuOpen() {\n//        return mSwipeRightHorizontal != null && mSwipeRightHorizontal.isMenuOpen(getScrollX());\n//    }\n//\n//    @Override\n//    public boolean isCompleteOpen() {\n//        return isLeftCompleteOpen() || isRightMenuOpen();\n//    }\n//\n//    @Override\n//    public boolean isLeftCompleteOpen() {\n//        return mSwipeLeftHorizontal != null && !mSwipeLeftHorizontal.isCompleteClose(getScrollX());\n//    }\n//\n//    @Override\n//    public boolean isRightCompleteOpen() {\n//        return mSwipeRightHorizontal != null && !mSwipeRightHorizontal.isCompleteClose(getScrollX());\n//    }\n//\n//    @Override\n//    public boolean isMenuOpenNotEqual() {\n//        return isLeftMenuOpenNotEqual() || isRightMenuOpenNotEqual();\n//    }\n//\n//    @Override\n//    public boolean isLeftMenuOpenNotEqual() {\n//        return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.isMenuOpenNotEqual(getScrollX());\n//    }\n//\n//    @Override\n//    public boolean isRightMenuOpenNotEqual() {\n//        return mSwipeRightHorizontal != null && mSwipeRightHorizontal.isMenuOpenNotEqual(getScrollX());\n//    }\n//\n//    @Override\n//    public void smoothOpenMenu() {\n//        smoothOpenMenu(mScrollerDuration);\n//    }\n//\n//    @Override\n//    public void smoothOpenLeftMenu() {\n//        smoothOpenLeftMenu(mScrollerDuration);\n//    }\n//\n//    @Override\n//    public void smoothOpenRightMenu() {\n//        smoothOpenRightMenu(mScrollerDuration);\n//    }\n//\n//    @Override\n//    public void smoothOpenLeftMenu(int duration) {\n//        if (mSwipeLeftHorizontal != null) {\n//            mSwipeCurrentHorizontal = mSwipeLeftHorizontal;\n//            smoothOpenMenu(duration);\n//        }\n//    }\n//\n//    @Override\n//    public void smoothOpenRightMenu(int duration) {\n//        if (mSwipeRightHorizontal != null) {\n//            mSwipeCurrentHorizontal = mSwipeRightHorizontal;\n//            smoothOpenMenu(duration);\n//        }\n//    }\n//\n//    private void smoothOpenMenu(int duration) {\n//        if (mSwipeCurrentHorizontal != null) {\n//            mSwipeCurrentHorizontal.autoOpenMenu(mScroller, getScrollX(), duration);\n//            invalidate();\n//        }\n//    }\n//\n//    @Override\n//    public void smoothCloseMenu() {\n//        smoothCloseMenu(mScrollerDuration);\n//    }\n//\n//    @Override\n//    public void smoothCloseLeftMenu() {\n//        if (mSwipeLeftHorizontal != null) {\n//            mSwipeCurrentHorizontal = mSwipeLeftHorizontal;\n//            smoothCloseMenu();\n//        }\n//    }\n//\n//    @Override\n//    public void smoothCloseRightMenu() {\n//        if (mSwipeRightHorizontal != null) {\n//            mSwipeCurrentHorizontal = mSwipeRightHorizontal;\n//            smoothCloseMenu();\n//        }\n//    }\n//\n//    @Override\n//    public void smoothCloseMenu(int duration) {\n//        if (mSwipeCurrentHorizontal != null) {\n//            mSwipeCurrentHorizontal.autoCloseMenu(mScroller, getScrollX(), duration);\n//            invalidate();\n//        }\n//    }\n//\n//    @Override\n//    protected void onLayout(boolean changed, int l, int t, int r, int b) {\n//        int contentViewHeight;\n//        if (mContentView != null) {\n//            int contentViewWidth = mContentView.getMeasuredWidthAndState();\n//            contentViewHeight = mContentView.getMeasuredHeightAndState();\n//            LayoutParams lp = (LayoutParams)mContentView.getLayoutParams();\n//            int start = getPaddingLeft();\n//            int top = getPaddingTop() + lp.topMargin;\n//            mContentView.layout(start, top, start + contentViewWidth, top + contentViewHeight);\n//        }\n//\n//        if (mSwipeLeftHorizontal != null) {\n//            View leftMenu = mSwipeLeftHorizontal.getMenuView();\n//            int menuViewWidth = leftMenu.getMeasuredWidthAndState();\n//            int menuViewHeight = leftMenu.getMeasuredHeightAndState();\n//            LayoutParams lp = (LayoutParams)leftMenu.getLayoutParams();\n//            int top = getPaddingTop() + lp.topMargin;\n//            leftMenu.layout(-menuViewWidth, top, 0, top + menuViewHeight);\n//        }\n//\n//        if (mSwipeRightHorizontal != null) {\n//            View rightMenu = mSwipeRightHorizontal.getMenuView();\n//            int menuViewWidth = rightMenu.getMeasuredWidthAndState();\n//            int menuViewHeight = rightMenu.getMeasuredHeightAndState();\n//            LayoutParams lp = (LayoutParams)rightMenu.getLayoutParams();\n//            int top = getPaddingTop() + lp.topMargin;\n//\n//            int parentViewWidth = getMeasuredWidthAndState();\n//            rightMenu.layout(parentViewWidth, top, parentViewWidth + menuViewWidth, top + menuViewHeight);\n//        }\n//    }\n//\n//}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/SwipeMenuView.java",
    "content": "/*\n * Copyright 2016 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview;\n\nimport android.content.Context;\nimport android.content.res.ColorStateList;\nimport android.graphics.Typeface;\nimport android.text.TextUtils;\nimport android.util.AttributeSet;\nimport android.util.TypedValue;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.widget.ImageView;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\n\nimport androidx.core.view.ViewCompat;\nimport androidx.core.widget.TextViewCompat;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport java.util.List;\n\n/**\n * Created by Yan Zhenjie on 2016/7/26.\n */\npublic class SwipeMenuView extends LinearLayout implements View.OnClickListener {\n\n    private RecyclerView.ViewHolder mViewHolder;\n    private OnItemMenuClickListener mItemClickListener;\n\n    public SwipeMenuView(Context context) {\n        this(context, null);\n    }\n\n    public SwipeMenuView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public SwipeMenuView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        setGravity(Gravity.CENTER_VERTICAL);\n    }\n\n    public void createMenu(RecyclerView.ViewHolder viewHolder, SwipeMenu swipeMenu, Controller controller,\n        int direction, OnItemMenuClickListener itemClickListener) {\n        removeAllViews();\n\n        this.mViewHolder = viewHolder;\n        this.mItemClickListener = itemClickListener;\n\n        List<SwipeMenuItem> items = swipeMenu.getMenuItems();\n        for (int i = 0; i < items.size(); i++) {\n            SwipeMenuItem item = items.get(i);\n\n            LayoutParams params = new LayoutParams(item.getWidth(), item.getHeight());\n            params.weight = item.getWeight();\n            params.setMargins(item.getLeftMargin(),item.getTopMargin(),item.getRightMargin(),item.getBottomMargin());\n            LinearLayout parent = new LinearLayout(getContext());\n            parent.setId(i);\n            parent.setGravity(Gravity.CENTER);\n            parent.setOrientation(VERTICAL);\n            parent.setLayoutParams(params);\n            ViewCompat.setBackground(parent, item.getBackground());\n            parent.setOnClickListener(this);\n            addView(parent);\n\n            SwipeMenuBridge menuBridge = new SwipeMenuBridge(controller, direction, i);\n            parent.setTag(menuBridge);\n\n            if (item.getImage() != null) {\n                ImageView iv = createIcon(item);\n                parent.addView(iv);\n            }\n\n            if (!TextUtils.isEmpty(item.getText())) {\n                TextView tv = createTitle(item);\n                parent.addView(tv);\n            }\n        }\n    }\n\n    @Override\n    public void onClick(View v) {\n        if (mItemClickListener != null) {\n            mItemClickListener.onItemClick((SwipeMenuBridge)v.getTag(), mViewHolder.getAdapterPosition());\n        }\n    }\n\n    private ImageView createIcon(SwipeMenuItem item) {\n        ImageView imageView = new ImageView(getContext());\n        imageView.setImageDrawable(item.getImage());\n        return imageView;\n    }\n\n    private TextView createTitle(SwipeMenuItem item) {\n        TextView textView = new TextView(getContext());\n        textView.setText(item.getText());\n        textView.setGravity(Gravity.CENTER);\n        int textSize = item.getTextSize();\n        if (textSize > 0) textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize);\n        ColorStateList textColor = item.getTitleColor();\n        if (textColor != null) textView.setTextColor(textColor);\n        int textAppearance = item.getTextAppearance();\n        if (textAppearance != 0) TextViewCompat.setTextAppearance(textView, textAppearance);\n        Typeface typeface = item.getTextTypeface();\n        if (typeface != null) textView.setTypeface(typeface);\n        return textView;\n    }\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/SwipeRecyclerView.java",
    "content": "/*\n * Copyright 2016 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewConfiguration;\nimport android.view.ViewGroup;\nimport android.view.ViewParent;\n\nimport androidx.annotation.IntDef;\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.StaggeredGridLayoutManager;\n\nimport com.yanzhenjie.recyclerview.widget.DefaultLoadMoreView;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Created by Yan Zhenjie on 2016/7/27.\n */\npublic class SwipeRecyclerView extends RecyclerView {\n\n\n    /**\n     * Left menu.\n     */\n    public static final int LEFT_DIRECTION = 1;\n    /**\n     * Right menu.\n     */\n    public static final int RIGHT_DIRECTION = -1;\n\n    @IntDef({LEFT_DIRECTION, RIGHT_DIRECTION})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface DirectionMode {}\n\n    /**\n     * Invalid position.\n     */\n    private static final int INVALID_POSITION = -1;\n\n    protected int mScaleTouchSlop;\n    protected SwipeDragLayout mOldSwipedLayout;\n    protected int mOldTouchedPosition = INVALID_POSITION;\n\n    private int mDownX;\n    private int mDownY;\n\n//    private boolean allowSwipeDelete;\n//    private DefaultItemTouchHelper mItemTouchHelper;\n\n    private SwipeMenuCreator mSwipeMenuCreator;\n    private OnItemMenuClickListener mOnItemMenuClickListener;\n    // TODO: 2019/4/14 新增\n    private OnItemSwipeListener mOnItemSwipeListener;\n\n\n    private OnItemClickListener mOnItemClickListener;\n    private OnItemLongClickListener mOnItemLongClickListener;\n    private AdapterWrapper mAdapterWrapper;\n\n    private boolean mSwipeItemMenuEnable = true;\n    private List<Integer> mDisableSwipeItemMenuList = new ArrayList<>();\n\n    public SwipeRecyclerView(Context context) {\n        this(context, null);\n    }\n\n    public SwipeRecyclerView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public SwipeRecyclerView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n        mScaleTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();\n    }\n\n\n//    private void initializeItemTouchHelper() {\n//        if (mItemTouchHelper == null) {\n//            mItemTouchHelper = new DefaultItemTouchHelper();\n//            mItemTouchHelper.attachToRecyclerView(this);\n//        }\n//    }\n\n//    /**\n//     * Set OnItemMoveListener.\n//     *\n//     * @param listener {@link OnItemMoveListener}.\n//     */\n//    public void setOnItemMoveListener(OnItemMoveListener listener) {\n//        initializeItemTouchHelper();\n//        this.mItemTouchHelper.setOnItemMoveListener(listener);\n//    }\n//\n//    /**\n//     * Set OnItemMovementListener.\n//     *\n//     * @param listener {@link OnItemMovementListener}.\n//     */\n//    public void setOnItemMovementListener(OnItemMovementListener listener) {\n//        initializeItemTouchHelper();\n//        this.mItemTouchHelper.setOnItemMovementListener(listener);\n//    }\n//\n//    /**\n//     * Set OnItemStateChangedListener.\n//     *\n//     * @param listener {@link OnItemStateChangedListener}.\n//     */\n//    public void setOnItemStateChangedListener(OnItemStateChangedListener listener) {\n//        initializeItemTouchHelper();\n//        this.mItemTouchHelper.setOnItemStateChangedListener(listener);\n//    }\n//\n//    /**\n//     * Set the item menu to enable status.\n//     *\n//     * @param enabled true means available, otherwise not available; default is true.\n//     */\n//    public void setSwipeItemMenuEnabled(boolean enabled) {\n//        this.mSwipeItemMenuEnable = enabled;\n//    }\n//\n//    /**\n//     * True means available, otherwise not available; default is true.\n//     */\n//    public boolean isSwipeItemMenuEnabled() {\n//        return mSwipeItemMenuEnable;\n//    }\n//\n//    /**\n//     * Set the item menu to enable status.\n//     *\n//     * @param position the position of the item.\n//     * @param enabled true means available, otherwise not available; default is true.\n//     */\n//    public void setSwipeItemMenuEnabled(int position, boolean enabled) {\n//        if (enabled) {\n//            if (mDisableSwipeItemMenuList.contains(position)) {\n//                mDisableSwipeItemMenuList.remove(Integer.valueOf(position));\n//            }\n//        } else {\n//            if (!mDisableSwipeItemMenuList.contains(position)) {\n//                mDisableSwipeItemMenuList.add(position);\n//            }\n//        }\n//    }\n//\n//    /**\n//     * True means available, otherwise not available; default is true.\n//     *\n//     * @param position the position of the item.\n//     */\n//    public boolean isSwipeItemMenuEnabled(int position) {\n//        return !mDisableSwipeItemMenuList.contains(position);\n//    }\n//\n//    /**\n//     * Set can long press drag.\n//     *\n//     * @param canDrag drag true, otherwise is can't.\n//     */\n//    public void setLongPressDragEnabled(boolean canDrag) {\n//        initializeItemTouchHelper();\n//        this.mItemTouchHelper.setLongPressDragEnabled(canDrag);\n//    }\n//\n//    /**\n//     * Get can long press drag.\n//     *\n//     * @return drag true, otherwise is can't.\n//     */\n//    public boolean isLongPressDragEnabled() {\n//        initializeItemTouchHelper();\n//        return this.mItemTouchHelper.isLongPressDragEnabled();\n//    }\n//\n//\n//    /**\n//     * Set can swipe delete.\n//     *\n//     * @param canSwipe swipe true, otherwise is can't.\n//     */\n//    public void setItemViewSwipeEnabled(boolean canSwipe) {\n//        initializeItemTouchHelper();\n//        allowSwipeDelete = canSwipe; // swipe and menu conflict.\n//        this.mItemTouchHelper.setItemViewSwipeEnabled(canSwipe);\n//    }\n//\n//    /**\n//     * Get can long press swipe.\n//     *\n//     * @return swipe true, otherwise is can't.\n//     */\n//    public boolean isItemViewSwipeEnabled() {\n//        initializeItemTouchHelper();\n//        return this.mItemTouchHelper.isItemViewSwipeEnabled();\n//    }\n//\n//    /**\n//     * Start drag a item.\n//     *\n//     * @param viewHolder the ViewHolder to start dragging. It must be a direct child of RecyclerView.\n//     */\n//    public void startDrag(ViewHolder viewHolder) {\n//        initializeItemTouchHelper();\n//        this.mItemTouchHelper.startDrag(viewHolder);\n//    }\n//\n//    /**\n//     * Star swipe a item.\n//     *\n//     * @param viewHolder the ViewHolder to start swiping. It must be a direct child of RecyclerView.\n//     */\n//    public void startSwipe(ViewHolder viewHolder) {\n//        initializeItemTouchHelper();\n//        this.mItemTouchHelper.startSwipe(viewHolder);\n//    }\n\n    /**\n     * Check the Adapter and throw an exception if it already exists.\n     */\n    private void checkAdapterExist(String message) {\n        if (mAdapterWrapper != null) throw new IllegalStateException(message);\n    }\n\n    /**\n     * Set item click listener.\n     */\n    public void setOnItemClickListener(OnItemClickListener listener) {\n        if (listener == null) return;\n        checkAdapterExist(\"Cannot set item click listener, setAdapter has already been called.\");\n        this.mOnItemClickListener = new ItemClickListener(this, listener);\n    }\n\n    private static class ItemClickListener implements OnItemClickListener {\n        private SwipeRecyclerView mRecyclerView;\n        private OnItemClickListener mListener;\n\n        public ItemClickListener(SwipeRecyclerView recyclerView, OnItemClickListener listener) {\n            this.mRecyclerView = recyclerView;\n            this.mListener = listener;\n        }\n\n        @Override\n        public void onItemClick(View itemView, int position) {\n            position -= mRecyclerView.getHeaderCount();\n            if (position >= 0) mListener.onItemClick(itemView, position);\n        }\n    }\n\n    /**\n     * Set item click listener.\n     */\n    public void setOnItemLongClickListener(OnItemLongClickListener listener) {\n        if (listener == null) return;\n        checkAdapterExist(\"Cannot set item long click listener, setAdapter has already been called.\");\n        this.mOnItemLongClickListener = new ItemLongClickListener(this, listener);\n    }\n\n    private static class ItemLongClickListener implements OnItemLongClickListener {\n\n        private SwipeRecyclerView mRecyclerView;\n        private OnItemLongClickListener mListener;\n\n        public ItemLongClickListener(SwipeRecyclerView recyclerView, OnItemLongClickListener listener) {\n            this.mRecyclerView = recyclerView;\n            this.mListener = listener;\n        }\n\n        @Override\n        public void onItemLongClick(View itemView, int position) {\n            position -= mRecyclerView.getHeaderCount();\n            if (position >= 0) mListener.onItemLongClick(itemView, position);\n        }\n    }\n\n    /**\n     * Set to create menu listener.\n     */\n    public void setSwipeMenuCreator(SwipeMenuCreator menuCreator) {\n        if (menuCreator == null) return;\n        checkAdapterExist(\"Cannot set menu creator, setAdapter has already been called.\");\n        this.mSwipeMenuCreator = menuCreator;\n    }\n\n    /**\n     * Set to click menu listener.\n     */\n    public void setOnItemMenuClickListener(OnItemMenuClickListener listener) {\n        if (listener == null) return;\n        checkAdapterExist(\"Cannot set menu item click listener, setAdapter has already been called.\");\n        this.mOnItemMenuClickListener = new ItemMenuClickListener(this, listener);\n    }\n\n\n    // todo: 新增\n    public void setOnItemSwipeListener(OnItemSwipeListener listener) {\n//        Log.e(\"SwiperRecycler\", \"设置监听器\" + listener );\n        if (listener == null) return;\n        checkAdapterExist(\"Cannot set menu item click listener, setAdapter has already been called.\");\n        this.mOnItemSwipeListener = new SimpleItemSwipeListener(this, listener);\n//        Log.e(\"SwiperRecycler\", \"设置监听器BB \" + mOnItemSwipeListener );\n    }\n\n\n    private static class SimpleItemSwipeListener implements OnItemSwipeListener {\n        private SwipeRecyclerView mRecyclerView;\n        private OnItemSwipeListener mListener;\n\n        public SimpleItemSwipeListener(SwipeRecyclerView recyclerView, OnItemSwipeListener listener) {\n            this.mRecyclerView = recyclerView;\n            this.mListener = listener;\n        }\n\n        @Override\n        public void onClose(View swipeMenu, int direction, int adapterPosition) {\n            adapterPosition -= mRecyclerView.getHeaderCount();\n            if (adapterPosition >= 0) {\n                mListener.onClose(swipeMenu, direction, adapterPosition);\n            }\n        }\n\n        @Override\n        public void onCloseLeft(int position) {\n            position -= mRecyclerView.getHeaderCount();\n            if (position >= 0) {\n                mListener.onCloseLeft(position);\n            }\n        }\n        @Override\n        public void onCloseRight(int position) {\n            position -= mRecyclerView.getHeaderCount();\n            if (position >= 0) {\n                mListener.onCloseRight(position);\n            }\n        }\n    }\n\n    private static class ItemMenuClickListener implements OnItemMenuClickListener {\n\n        private SwipeRecyclerView mRecyclerView;\n        private OnItemMenuClickListener mListener;\n\n        public ItemMenuClickListener(SwipeRecyclerView recyclerView, OnItemMenuClickListener listener) {\n            this.mRecyclerView = recyclerView;\n            this.mListener = listener;\n        }\n\n        @Override\n        public void onItemClick(SwipeMenuBridge menuBridge, int position) {\n            position -= mRecyclerView.getHeaderCount();\n            if (position >= 0) {\n                mListener.onItemClick(menuBridge, position);\n            }\n        }\n    }\n\n    @Override\n    public void setLayoutManager(LayoutManager layoutManager) {\n        if (layoutManager instanceof GridLayoutManager) {\n            final GridLayoutManager gridLayoutManager = (GridLayoutManager)layoutManager;\n            final GridLayoutManager.SpanSizeLookup spanSizeLookupHolder = gridLayoutManager.getSpanSizeLookup();\n\n            gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {\n                @Override\n                public int getSpanSize(int position) {\n                    if (mAdapterWrapper.isHeader(position) || mAdapterWrapper.isFooter(position)) {\n                        return gridLayoutManager.getSpanCount();\n                    }\n                    if (spanSizeLookupHolder != null) {\n                        return spanSizeLookupHolder.getSpanSize(position - getHeaderCount());\n                    }\n                    return 1;\n                }\n            });\n        }\n        super.setLayoutManager(layoutManager);\n    }\n\n//    /**\n//     * Get the original adapter.\n//     */\n//    public Adapter getOriginAdapter() {\n//        if (mAdapterWrapper == null) return null;\n//        return mAdapterWrapper.getOriginAdapter();\n//    }\n\n    @Override\n    public void setAdapter(Adapter adapter) {\n        if (mAdapterWrapper != null) {\n            mAdapterWrapper.getOriginAdapter().unregisterAdapterDataObserver(mAdapterDataObserver);\n        }\n\n        if (adapter == null) {\n            mAdapterWrapper = null;\n        } else {\n            adapter.registerAdapterDataObserver(mAdapterDataObserver);\n\n            mAdapterWrapper = new AdapterWrapper(getContext(), adapter);\n            mAdapterWrapper.setOnItemClickListener(mOnItemClickListener);\n            mAdapterWrapper.setOnItemLongClickListener(mOnItemLongClickListener);\n            mAdapterWrapper.setSwipeMenuCreator(mSwipeMenuCreator);\n            mAdapterWrapper.setOnItemMenuClickListener(mOnItemMenuClickListener);\n            // TODO: 2019/4/14 新增\n            mAdapterWrapper.setOnItemSwipeListener(mOnItemSwipeListener);\n\n            if (mHeaderViewList.size() > 0) {\n                for (View view : mHeaderViewList) {\n                    mAdapterWrapper.addHeaderView(view);\n                }\n\n            }\n            if (mFooterViewList.size() > 0) {\n                for (View view : mFooterViewList) {\n                    mAdapterWrapper.addFooterView(view);\n                }\n            }\n        }\n        super.setAdapter(mAdapterWrapper);\n    }\n\n    private AdapterDataObserver mAdapterDataObserver = new AdapterDataObserver() {\n        @Override\n        public void onChanged() {\n            mAdapterWrapper.notifyDataSetChanged();\n        }\n\n        @Override\n        public void onItemRangeChanged(int positionStart, int itemCount) {\n            positionStart += getHeaderCount();\n            mAdapterWrapper.notifyItemRangeChanged(positionStart, itemCount);\n        }\n\n        @Override\n        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {\n            positionStart += getHeaderCount();\n            mAdapterWrapper.notifyItemRangeChanged(positionStart, itemCount, payload);\n        }\n\n        @Override\n        public void onItemRangeInserted(int positionStart, int itemCount) {\n            positionStart += getHeaderCount();\n            mAdapterWrapper.notifyItemRangeInserted(positionStart, itemCount);\n        }\n\n        @Override\n        public void onItemRangeRemoved(int positionStart, int itemCount) {\n            positionStart += getHeaderCount();\n            mAdapterWrapper.notifyItemRangeRemoved(positionStart, itemCount);\n        }\n\n        @Override\n        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {\n            fromPosition += getHeaderCount();\n            toPosition += getHeaderCount();\n            mAdapterWrapper.notifyItemMoved(fromPosition, toPosition);\n        }\n    };\n\n    private List<View> mHeaderViewList = new ArrayList<>();\n    private List<View> mFooterViewList = new ArrayList<>();\n\n\n    /**\n     * Add view at the headers.\n     */\n    public void addHeaderView(View view) {\n        mHeaderViewList.add(view);\n        if (mAdapterWrapper != null) {\n            mAdapterWrapper.addHeaderViewAndNotify(view);\n        }\n    }\n\n    /**\n     * Remove view from header.\n     */\n    public void removeHeaderView(View view) {\n        mHeaderViewList.remove(view);\n        if (mAdapterWrapper != null) {\n            mAdapterWrapper.removeHeaderViewAndNotify(view);\n        }\n    }\n\n    /**\n     * Add view at the footer.\n     */\n    public void addFooterView(View view) {\n        mFooterViewList.add(view);\n        if (mAdapterWrapper != null) {\n            mAdapterWrapper.addFooterViewAndNotify(view);\n        }\n    }\n\n    public void removeFooterView(View view) {\n        mFooterViewList.remove(view);\n        if (mAdapterWrapper != null) {\n            mAdapterWrapper.removeFooterViewAndNotify(view);\n        }\n    }\n\n    /**\n     * Get size of headers.\n     */\n    public int getHeaderCount() {\n        if (mAdapterWrapper == null) return 0;\n        return mAdapterWrapper.getHeaderCount();\n    }\n\n\n    @Override\n    public boolean onInterceptTouchEvent(MotionEvent e) {\n        boolean isIntercepted = super.onInterceptTouchEvent(e);\n        if (mSwipeMenuCreator == null) { // allowSwipeDelete ||\n            return isIntercepted;\n        } else {\n            if (e.getPointerCount() > 1) return true;\n            int action = e.getAction();\n            int x = (int)e.getX();\n            int y = (int)e.getY();\n\n            int touchPosition = getChildAdapterPosition(findChildViewUnder(x, y));\n            ViewHolder touchVH = findViewHolderForAdapterPosition(touchPosition);\n            SwipeDragLayout touchView = null;\n            if (touchVH != null) {\n                View itemView = getSwipeMenuView(touchVH.itemView);\n                if (itemView instanceof SwipeDragLayout) {\n                    touchView = (SwipeDragLayout)itemView;\n                }\n            }\n\n            boolean touchMenuEnable = mSwipeItemMenuEnable && !mDisableSwipeItemMenuList.contains(touchPosition);\n//            if (touchView != null) {\n//                touchView.setSwipeEnable(touchMenuEnable);\n//            }\n\n            if (!touchMenuEnable) return isIntercepted;\n\n            switch (action) {\n                case MotionEvent.ACTION_DOWN: {\n                    mDownX = x;\n                    mDownY = y;\n\n                    isIntercepted = false;\n                    if (touchPosition != mOldTouchedPosition && mOldSwipedLayout != null &&\n                        mOldSwipedLayout.isMenuOpen()) {\n                        mOldSwipedLayout.smoothCloseMenu();\n                        isIntercepted = true;\n                    }\n\n                    if (isIntercepted) {\n                        mOldSwipedLayout = null;\n                        mOldTouchedPosition = INVALID_POSITION;\n                    } else if (touchView != null) {\n                        mOldSwipedLayout = touchView;\n                        mOldTouchedPosition = touchPosition;\n                    }\n                    break;\n                }\n                // They are sensitive to retain sliding and inertia.\n                case MotionEvent.ACTION_MOVE: {\n                    isIntercepted = handleUnDown(x, y, isIntercepted);\n                    if (mOldSwipedLayout == null) break;\n                    ViewParent viewParent = getParent();\n                    if (viewParent == null) break;\n\n                    int disX = mDownX - x;\n//                    // 向左滑，显示右侧菜单，或者关闭左侧菜单。\n//                    boolean showRightCloseLeft = disX > 0 &&\n//                        (mOldSwipedLayout.hasRightMenu() || mOldSwipedLayout.isLeftCompleteOpen());\n//                    // 向右滑，显示左侧菜单，或者关闭右侧菜单。\n//                    boolean showLeftCloseRight = disX < 0 &&\n//                        (mOldSwipedLayout.hasLeftMenu() || mOldSwipedLayout.isRightCompleteOpen());\n\n                    boolean showRightCloseLeft = disX > 0;\n                    boolean showLeftCloseRight = disX < 0;\n                    viewParent.requestDisallowInterceptTouchEvent(showRightCloseLeft || showLeftCloseRight);\n                }\n                case MotionEvent.ACTION_UP:\n                case MotionEvent.ACTION_CANCEL: {\n                    isIntercepted = handleUnDown(x, y, isIntercepted);\n//                    Log.e(\"测试侧滑\", \"onInterceptTouchEvent：\" +  action + isIntercepted  );\n                    break;\n                }\n            }\n        }\n        return isIntercepted;\n    }\n\n    private boolean handleUnDown(int x, int y, boolean defaultValue) {\n        int disX = mDownX - x;\n        int disY = mDownY - y;\n\n        // swipe\n        if (Math.abs(disX) > mScaleTouchSlop && Math.abs(disX) > Math.abs(disY)) return false;\n        // click\n        if (Math.abs(disY) < mScaleTouchSlop && Math.abs(disX) < mScaleTouchSlop) return false;\n        return defaultValue;\n    }\n\n    @Override\n    public boolean onTouchEvent(MotionEvent e) {\n        int action = e.getAction();\n        switch (action) {\n            case MotionEvent.ACTION_DOWN:\n                break;\n            case MotionEvent.ACTION_MOVE:\n                if (mOldSwipedLayout != null && mOldSwipedLayout.isMenuOpen()) {\n                    mOldSwipedLayout.smoothCloseMenu();\n                }\n                break;\n            case MotionEvent.ACTION_UP:\n                break;\n            case MotionEvent.ACTION_CANCEL:\n                break;\n        }\n        return super.onTouchEvent(e);\n    }\n\n    private View getSwipeMenuView(View itemView) {\n        if (itemView instanceof SwipeDragLayout) return itemView;\n        List<View> unvisited = new ArrayList<>();\n        unvisited.add(itemView);\n        while (!unvisited.isEmpty()) {\n            View child = unvisited.remove(0);\n            if (!(child instanceof ViewGroup)) { // view\n                continue;\n            }\n            if (child instanceof SwipeDragLayout) return child;\n            ViewGroup group = (ViewGroup)child;\n            final int childCount = group.getChildCount();\n            for (int i = 0; i < childCount; i++) unvisited.add(group.getChildAt(i));\n        }\n        return itemView;\n    }\n\n    private int mScrollState = -1;\n\n    private boolean isLoadMore = false;\n    private boolean isAutoLoadMore = true;\n    private boolean isLoadError = false;\n\n    private boolean mDataEmpty = true;\n    private boolean mHasMore = false;\n\n    private LoadMoreView mLoadMoreView;\n    private LoadMoreListener mLoadMoreListener;\n\n    @Override\n    public void onScrollStateChanged(int state) {\n        this.mScrollState = state;\n    }\n\n    @Override\n    public void onScrolled(int dx, int dy) {\n        LayoutManager layoutManager = getLayoutManager();\n        if (layoutManager instanceof LinearLayoutManager) {\n            LinearLayoutManager linearLayoutManager = (LinearLayoutManager)layoutManager;\n\n            int itemCount = layoutManager.getItemCount();\n            if (itemCount <= 0) return;\n\n            int lastVisiblePosition = linearLayoutManager.findLastVisibleItemPosition();\n\n            if (itemCount == lastVisiblePosition + 1 &&\n                (mScrollState == SCROLL_STATE_DRAGGING || mScrollState == SCROLL_STATE_SETTLING)) {\n                dispatchLoadMore();\n            }\n        } else if (layoutManager instanceof StaggeredGridLayoutManager) {\n            StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)layoutManager;\n\n            int itemCount = layoutManager.getItemCount();\n            if (itemCount <= 0) return;\n\n            int[] lastVisiblePositionArray = staggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(null);\n            int lastVisiblePosition = lastVisiblePositionArray[lastVisiblePositionArray.length - 1];\n\n            if (itemCount == lastVisiblePosition + 1 &&\n                (mScrollState == SCROLL_STATE_DRAGGING || mScrollState == SCROLL_STATE_SETTLING)) {\n                dispatchLoadMore();\n            }\n        }\n    }\n\n    private void dispatchLoadMore() {\n        if (isLoadError) return;\n\n        if (!isAutoLoadMore) {\n            if (mLoadMoreView != null) mLoadMoreView.onWaitToLoadMore(mLoadMoreListener);\n        } else {\n            if (isLoadMore || mDataEmpty || !mHasMore) return;\n\n            isLoadMore = true;\n\n            if (mLoadMoreView != null) mLoadMoreView.onLoading();\n\n            if (mLoadMoreListener != null) mLoadMoreListener.onLoadMore();\n        }\n    }\n\n    /**\n     * Use the default to load more View.\n     */\n    public void useDefaultLoadMore() {\n        DefaultLoadMoreView defaultLoadMoreView = new DefaultLoadMoreView(getContext());\n        addFooterView(defaultLoadMoreView);\n        setLoadMoreView(defaultLoadMoreView);\n    }\n\n    /**\n     * Load more view.\n     */\n    public void setLoadMoreView(LoadMoreView view) {\n        mLoadMoreView = view;\n    }\n\n    /**\n     * Load more listener.\n     */\n    public void setLoadMoreListener(LoadMoreListener listener) {\n        mLoadMoreListener = listener;\n    }\n\n    /**\n     * Automatically load more automatically.\n     * <p>\n     * Non-auto-loading mode, you can to click on the item to load.\n     * </p>\n     *\n     * @param autoLoadMore you can use false.\n     *\n     * @see LoadMoreView#onWaitToLoadMore(LoadMoreListener)\n     */\n    public void setAutoLoadMore(boolean autoLoadMore) {\n        isAutoLoadMore = autoLoadMore;\n    }\n\n    /**\n     * Load more done.\n     *\n     * @param dataEmpty data is empty ?\n     * @param hasMore has more data ?\n     */\n    public final void loadMoreFinish(boolean dataEmpty, boolean hasMore) {\n        isLoadMore = false;\n        isLoadError = false;\n\n        mDataEmpty = dataEmpty;\n        mHasMore = hasMore;\n\n        if (mLoadMoreView != null) {\n            mLoadMoreView.onLoadFinish(dataEmpty, hasMore);\n        }\n    }\n\n    /**\n     * Called when data is loaded incorrectly.\n     *\n     * @param errorCode Error code, will be passed to the LoadView, you can according to it to customize the prompt\n     *     information.\n     * @param errorMessage Error message.\n     */\n    public void loadMoreError(int errorCode, String errorMessage) {\n        isLoadMore = false;\n        isLoadError = true;\n\n        if (mLoadMoreView != null) {\n            mLoadMoreView.onLoadError(errorCode, errorMessage);\n        }\n    }\n\n    public interface LoadMoreView {\n\n        /**\n         * Show progress.\n         */\n        void onLoading();\n\n        /**\n         * Load finish, handle result.\n         */\n        void onLoadFinish(boolean dataEmpty, boolean hasMore);\n\n        /**\n         * Non-auto-loading mode, you can to click on the item to load.\n         */\n        void onWaitToLoadMore(LoadMoreListener loadMoreListener);\n\n        /**\n         * Load error.\n         */\n        void onLoadError(int errorCode, String errorMessage);\n    }\n\n    public interface LoadMoreListener {\n\n        /**\n         * More data should be requested.\n         */\n        void onLoadMore();\n    }\n\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/listview/FastScrollDelegate.java",
    "content": "package com.yanzhenjie.recyclerview.listview;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.ColorFilter;\nimport android.graphics.Interpolator;\nimport android.graphics.Paint;\nimport android.graphics.Path;\nimport android.graphics.PixelFormat;\nimport android.graphics.Rect;\nimport android.graphics.RectF;\nimport android.graphics.drawable.Drawable;\nimport android.graphics.drawable.GradientDrawable;\nimport android.graphics.drawable.InsetDrawable;\nimport android.graphics.drawable.StateListDrawable;\nimport android.text.TextUtils.TruncateAt;\nimport android.util.Log;\nimport android.util.TypedValue;\nimport android.view.Gravity;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewConfiguration;\nimport android.view.animation.AnimationUtils;\nimport android.widget.AbsListView;\nimport android.widget.PopupWindow;\nimport android.widget.TextView;\n\nimport androidx.core.view.ViewCompat;\n\n/**\n * 实现 ListView ，WebView 等的快速滚动条\n * https://github.com/Mixiaoxiao/FastScroll-Everywhere FastScrollDelegate\n *\n * @author Mixiaoxiao 2016-08-28\n */\npublic class FastScrollDelegate {\n\n    @SuppressWarnings(\"unused\")\n    private void log(String msg) {\n        Log.d(\"FastScrollDelegate\", msg);\n    }\n\n    public interface FastScrollable {\n\n        void superOnTouchEvent(MotionEvent event);\n\n        int superComputeVerticalScrollExtent();\n\n        int superComputeVerticalScrollOffset();\n\n        int superComputeVerticalScrollRange();\n\n        View getFastScrollableView();\n\n        FastScrollDelegate getFastScrollDelegate();\n\n        void setNewFastScrollDelegate(FastScrollDelegate newDelegate);\n    }\n\n    public interface OnFastScrollListener {\n        void onFastScrollStart(View view, FastScrollDelegate delegate);\n\n        void onFastScrolled(View view, FastScrollDelegate delegate, int touchDeltaY, int viewScrollDeltaY,\n                            float scrollPercent);\n\n        void onFastScrollEnd(View view, FastScrollDelegate delegate);\n    }\n\n    // codes from FastScroller (for AbsListView)\n    /**\n     * Duration of fade-out animation.\n     */\n    public static int FASTSCROLLER_DURATION_FADE_OUT = 300;\n    /**\n     * Duration of fade-in animation.\n     */\n    public static int FASTSCROLLER_DURATION_FADE_IN = 150;\n    /**\n     * Inactivity timeout before fading controls.\n     */\n    public static long FASTSCROLLER_FADE_TIMEOUT = 1500;\n\n    private static final int[] DRAWABLE_STATE_PRESSED = new int[]{android.R.attr.state_pressed};\n    private static final int[] DRAWABLE_STATE_DEFAULT = new int[]{};\n\n    private final View mView;\n    private final float mDensity;\n    private float mDownY;\n    private final Rect mThumbRect;\n    private Drawable mThumbDrawable;\n    private final FastScrollable mFastScrollable;\n    private int mThumbMinHeight;\n    private final ScrollabilityCache mScrollCache;\n    private IndicatorPopup mIndicatorPopup;\n    private boolean mThumbDynamicHeight;\n\n    private OnFastScrollListener mFastScrollListener;\n    private boolean mIsHanlingTouchEvent = false;\n\n    private FastScrollDelegate(final FastScrollable fastScrollable, int width, int height, Drawable thumbDrawable,\n                               boolean isDynamicHeight) {\n        super();\n        this.mView = fastScrollable.getFastScrollableView();\n        mView.setVerticalScrollBarEnabled(false);\n        Context context = mView.getContext();\n        this.mDensity = context.getResources().getDisplayMetrics().density;\n        this.mThumbMinHeight = dp2px(FASTSCROLLER_MIN_HEIGHT_DP);\n        this.mThumbRect = new Rect(0, 0, width, height);\n        this.mThumbDrawable = thumbDrawable;\n        this.mFastScrollable = fastScrollable;\n        this.mScrollCache = new ScrollabilityCache(ViewConfiguration.get(context), mView);\n        this.mThumbDynamicHeight = isDynamicHeight;\n    }\n\n    // ===========================================================\n    // Useful methods\n    // ===========================================================\n    public void setThumbDrawable(Drawable drawable) {\n        if (drawable == null) {\n            throw new IllegalArgumentException(\"setThumbDrawable must NOT be NULL\");\n        }\n        mThumbDrawable = drawable;\n        updateThumbRect(0);\n    }\n\n    public void setThumbSize(int widthDp, int heightDp) {\n        mThumbRect.left = mThumbRect.right - dp2px(widthDp);\n        mThumbMinHeight = dp2px(heightDp);\n        updateThumbRect(0);\n    }\n\n    public void setThumbDynamicHeight(boolean isDynamicHeight) {\n        if (mThumbDynamicHeight != isDynamicHeight) {\n            mThumbDynamicHeight = isDynamicHeight;\n            updateThumbRect(0);\n        }\n    }\n\n    public void setOnFastScrollListener(OnFastScrollListener l) {\n        mFastScrollListener = l;\n    }\n\n    // ===========================================================\n    // Delegate\n    // ===========================================================\n\n    // See View.class\n    public boolean awakenScrollBars() {\n        return awakenScrollBars(FASTSCROLLER_FADE_TIMEOUT);// Cache.scrollBarDefaultDelayBeforeFade\n    }\n\n    // See View.class\n    private boolean initialAwakenScrollBars() {\n        return awakenScrollBars(mScrollCache.scrollBarDefaultDelayBeforeFade * 4);\n    }\n\n    // See View.class\n    public boolean awakenScrollBars(long startDelay) {\n        ViewCompat.postInvalidateOnAnimation(mView);\n        // log(\"awakenScrollBars call startDelay->\" + startDelay);\n        if (!mIsHanlingTouchEvent) {\n            if (mScrollCache.state == ScrollabilityCache.OFF) {\n                // FIXME: this is copied from WindowManagerService.\n                // We should get this value from the system when it\n                // is possible to do so.\n                final int KEY_REPEAT_FIRST_DELAY = 750;\n                startDelay = Math.max(KEY_REPEAT_FIRST_DELAY, startDelay);\n            }\n            // Tell mScrollCache when we should start fading. This may\n            // extend the fade start time if one was already scheduled\n            long fadeStartTime = AnimationUtils.currentAnimationTimeMillis() + startDelay;\n            mScrollCache.fadeStartTime = fadeStartTime;\n            mScrollCache.state = ScrollabilityCache.ON;\n            // Schedule our fader to run, unscheduling any old ones first\n            // if (mAttachInfo != null) {\n            // mAttachInfo.mHandler.removeCallbacks(scrollCache);\n            // mAttachInfo.mHandler.postAtTime(scrollCache, fadeStartTime);\n            // }\n            mView.removeCallbacks(mScrollCache);\n            mView.postDelayed(mScrollCache, fadeStartTime - AnimationUtils.currentAnimationTimeMillis());\n        }\n        return false;\n    }\n\n    // ===========================================================\n    // TouchEvent Delegate\n    // ===========================================================\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n        return onInterceptTouchEventInternal(ev);\n    }\n\n    public boolean onTouchEvent(MotionEvent event) {\n        return onTouchEventInternal(event);\n    }\n\n    // ===========================================================\n    // TouchEvent Internal\n    // ===========================================================\n    private boolean onInterceptTouchEventInternal(MotionEvent ev) {\n        final int action = ev.getActionMasked();\n        if (action == MotionEvent.ACTION_DOWN) {\n            // Just check if hit the thumb\n            return onTouchEventInternal(ev);\n        }\n        return false;\n    }\n\n    private boolean onTouchEventInternal(MotionEvent event) {\n//        KLog.e(\"执行父onTouchEventInternal：\"  );\n        final int action = event.getActionMasked();\n        final float y = event.getY();\n        switch (action) {\n            case MotionEvent.ACTION_DOWN: {\n                // log(\"onTouchEvent ACTION_DOWN\");\n                if (mScrollCache.state == ScrollabilityCache.OFF) {\n                    mIsHanlingTouchEvent = false;\n                    return false;\n                }\n                if (!mIsHanlingTouchEvent) {\n                    updateThumbRect(0);\n                    final float x = event.getX();\n                    // Check if hit the thumb, Rect.contains(int x ,int y) is NOT\n                    // exact\n                    if (y >= mThumbRect.top && y <= mThumbRect.bottom && x >= mThumbRect.left && x <= mThumbRect.right) {\n                        mIsHanlingTouchEvent = true;\n                        mDownY = y;\n                        // try to stop scroll\n                        // step 0: call super ACTION_DOWN\n                        mFastScrollable.superOnTouchEvent(event);\n                        // step 1: call super ACTION_CANCEL\n                        MotionEvent fakeCancelMotionEvent = MotionEvent.obtain(event);\n                        fakeCancelMotionEvent.setAction(MotionEvent.ACTION_CANCEL);\n                        mFastScrollable.superOnTouchEvent(fakeCancelMotionEvent);\n                        fakeCancelMotionEvent.recycle();\n                        // update ThumbDrawable state and report\n                        // OnFastScrollListener\n                        setPressedThumb(true);\n                        // Call updateThumbRect to report\n                        // OnFastScrollListener.onFastScrolled\n                        updateThumbRect(0, true);\n                        // Do NOT fade Thumb\n                        mView.removeCallbacks(mScrollCache);\n                    }\n                }\n                break;\n            }\n            case MotionEvent.ACTION_MOVE: {\n                if (mIsHanlingTouchEvent) {\n                    final int touchDeltaY = Math.round(y - mDownY);\n                    if (touchDeltaY != 0) {\n                        updateThumbRect(touchDeltaY);\n                        // only touchDeltaY != 0, we save the touchY, to Avoid\n                        // accuracy error\n                        mDownY = y;\n                    }\n                }\n                break;\n            }\n            case MotionEvent.ACTION_UP:\n            case MotionEvent.ACTION_CANCEL: {\n                if (mIsHanlingTouchEvent) {\n                    setPressedThumb(false);\n                    mIsHanlingTouchEvent = false;\n                    awakenScrollBars();\n                }\n                break;\n            }\n            default:\n                break;\n        }// End switch\n        if (mIsHanlingTouchEvent) {\n            mView.invalidate();\n            mView.getParent().requestDisallowInterceptTouchEvent(true);\n            return true;\n        }\n        return false;\n    }\n\n    // ===========================================================\n    // Delegate\n    // ===========================================================\n\n    /**\n     * Call after View.dispatchDraw()\n     **/\n    public void dispatchDrawOver(Canvas canvas) {\n        onDrawScrollBars(canvas);\n    }\n\n    public void onAttachedToWindow() {\n        initialAwakenScrollBars();\n    }\n\n    @SuppressLint(\"MissingSuperCall\")\n    // fuck this lint warning\n    public void onDetachedFromWindow() {\n        if (mIndicatorPopup != null) {\n            mIndicatorPopup.dismiss();\n        }\n    }\n\n    /**\n     * Please check if the delegate is NULL before call this method If your view\n     * has the android:visibility attr in xml, this method in view is called\n     * before your delegate is created\n     */\n    public void onVisibilityChanged(View changedView, int visibility) {\n        if (visibility == View.VISIBLE) {\n            // This compat method is interesting, KK has method\n            // isAttachedToWindow\n            // < KK is view.getWindowToken() != null\n            if (ViewCompat.isAttachedToWindow(mView)) {\n                // Same as mAttachInfo != null\n                initialAwakenScrollBars();\n            }\n\n        }\n    }\n\n    public void onWindowVisibilityChanged(int visibility) {\n        if (visibility == View.VISIBLE) {\n            initialAwakenScrollBars();\n        }\n    }\n\n    // ===========================================================\n    // Internal\n    // ===========================================================\n\n    private void onDrawScrollBars(Canvas canvas) {\n        boolean invalidate = false;\n        if (mIsHanlingTouchEvent) {\n            mThumbDrawable.setAlpha(255);\n        } else {\n            // Copy from View.class\n            final ScrollabilityCache cache = mScrollCache;\n            // cache.scrollBar = mThumbDrawable;\n            final int state = cache.state;\n            if (state == ScrollabilityCache.OFF) {\n                return;\n            }\n            if (state == ScrollabilityCache.FADING) {\n                // We're fading -- get our fade interpolation\n                if (cache.interpolatorValues == null) {\n                    cache.interpolatorValues = new float[1];\n                }\n                float[] values = cache.interpolatorValues;\n                // Stops the animation if we're done\n                if (cache.scrollBarInterpolator.timeToValues(values) == Interpolator.Result.FREEZE_END) {\n                    cache.state = ScrollabilityCache.OFF;\n                } else {\n                    // in View.class is \"cache.scrollBar.mutate()\"\n                    mThumbDrawable.setAlpha(Math.round(values[0]));\n                }\n                invalidate = true;\n            } else {\n                // reset alpha, in View.class is \"cache.scrollBar.mutate()\"\n                mThumbDrawable.setAlpha(255);\n            }\n        }\n\n        // Draw the thumb\n        if (updateThumbRect(0)) {\n            final int scrollY = mView.getScrollY();\n            final int scrollX = mView.getScrollX();\n            mThumbDrawable.setBounds(mThumbRect.left + scrollX, mThumbRect.top + scrollY, mThumbRect.right + scrollX,\n                    mThumbRect.bottom + scrollY);\n            mThumbDrawable.draw(canvas);\n        }\n        if (invalidate) {\n            mView.invalidate();\n        }\n\n    }\n\n    private void setPressedThumb(boolean pressed) {\n        mThumbDrawable.setState(pressed ? DRAWABLE_STATE_PRESSED : DRAWABLE_STATE_DEFAULT);\n        mView.invalidate();\n        if (mIndicatorPopup != null) {\n            if (pressed) {\n                mIndicatorPopup.show();\n            } else {\n                mIndicatorPopup.dismiss();\n            }\n        }\n        if (mFastScrollListener != null) {\n            if (pressed) {\n                mFastScrollListener.onFastScrollStart(mView, this);\n            } else {\n                mFastScrollListener.onFastScrollEnd(mView, this);\n            }\n\n        }\n    }\n\n    private boolean updateThumbRect(int touchDeltaY) {\n        return updateThumbRect(touchDeltaY, false);\n    }\n\n    /**\n     * updateThumbRect\n     *\n     * @param touchDeltaY             ,if touchDeltaY != 0, will report\n     *                                FastScrollListener.onFastScrolled\n     * @param forceReportFastScrolled , if true, will force report FastScrollListener.onFastScrolled\n     * @return false:Thumb return false means no need to draw thumb\n     */\n    private boolean updateThumbRect(int touchDeltaY, boolean forceReportFastScrolled) {\n        final int thumbWidth = mThumbRect.width();\n        mThumbRect.right = mView.getWidth();\n        mThumbRect.left = mThumbRect.right - thumbWidth;\n        final int scrollRange = mFastScrollable.superComputeVerticalScrollRange();// 整体的全部高度\n        if (scrollRange <= 0) {// no content, 仅在有内容的时候绘制thumb\n            return false;\n        }\n        final int scrollOffset = mFastScrollable.superComputeVerticalScrollOffset();// 上方已经滑动出本身范围的高度\n        final int scrollExtent = mFastScrollable.superComputeVerticalScrollExtent();// 当前显示区域的高度\n        final int scrollMaxOffset = scrollRange - scrollExtent;\n        if (scrollMaxOffset <= 0) {// can not scroll, 内容部分不够或刚好充满\n            return false;\n        }\n        final float scrollPercent = scrollOffset * 1f / (scrollMaxOffset);\n        final float visiblePercent = scrollExtent * 1f / scrollRange;\n        // log(\"scrollPercent->\" + scrollPercent + \" visiblePercent->\" +\n        // visiblePercent);\n        final int viewHeight = mView.getHeight();\n        final int thumbHeight = mThumbDynamicHeight ? Math\n                .max(mThumbMinHeight, Math.round(visiblePercent * viewHeight)) : mThumbMinHeight;\n        mThumbRect.bottom = mThumbRect.top + thumbHeight;\n        final int thumbTop = Math.round((viewHeight - thumbHeight) * scrollPercent);\n        mThumbRect.offsetTo(mThumbRect.left, thumbTop);\n\n        if (mIndicatorPopup != null) {\n            mIndicatorPopup.setOffset(mView.getWidth() - mIndicatorPopup.getPopupSize() - mThumbRect.width(),\n                    -viewHeight + mThumbRect.centerY() - mIndicatorPopup.getPopupSize());\n        }\n        if (touchDeltaY != 0) {// compute the ScrollOffset, 按touchDeltaY计算滚动\n            int newThumbTop = thumbTop + touchDeltaY;\n            final int minThumbTop = 0;\n            final int maxThumbTop = viewHeight - thumbHeight;\n            if (newThumbTop > maxThumbTop) {\n                newThumbTop = maxThumbTop;\n            } else if (newThumbTop < minThumbTop) {\n                newThumbTop = minThumbTop;\n            }\n\n            final float newScrollPercent = newThumbTop * 1f / maxThumbTop;// 百分比\n            final int newScrollOffset = Math.round((scrollRange - scrollExtent) * newScrollPercent);\n            final int viewScrollDeltaY = newScrollOffset - scrollOffset;\n            if (mView instanceof AbsListView) {\n                // Call scrollBy to AbsListView , not work correctly\n                ((AbsListView) mView).smoothScrollBy(viewScrollDeltaY, 0);\n            } else {\n                mView.scrollBy(0, viewScrollDeltaY);\n\n            }\n            if (mFastScrollListener != null) {\n                mFastScrollListener.onFastScrolled(mView, this, touchDeltaY, viewScrollDeltaY, newScrollPercent);\n            }\n        } else {\n            if (forceReportFastScrolled) {\n                if (mFastScrollListener != null) {\n                    mFastScrollListener.onFastScrolled(mView, this, 0, 0, scrollPercent);\n                }\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Copy from View.class\n     **/\n    private static class ScrollabilityCache implements Runnable {\n        /*** Scrollbars are not visible */\n        public static final int OFF = 0;\n        /**\n         * Scrollbars are visible\n         */\n        public static final int ON = 1;\n        /**\n         * Scrollbars are fading away\n         */\n        public static final int FADING = 2;\n        public final int scrollBarDefaultDelayBeforeFade;\n        public final int scrollBarFadeDuration;\n\n        // public ScrollBarDrawable scrollBar;\n        // public Drawable scrollBar;\n        public float[] interpolatorValues;\n        public View host;\n\n        public final Interpolator scrollBarInterpolator = new Interpolator(1, 2);\n\n        private static final float[] OPAQUE = {255};\n        private static final float[] TRANSPARENT = {0.0f};\n\n        /**\n         * When fading should start. This time moves into the future every time\n         * a new scroll happens. Measured based on SystemClock.uptimeMillis()\n         */\n        public long fadeStartTime;\n\n        /**\n         * The current state of the scrollbars: ON, OFF, or FADING\n         */\n        public int state = OFF;\n\n        public ScrollabilityCache(ViewConfiguration configuration, View host) {\n            // scrollBarSize = configuration.getScaledScrollBarSize();\n            scrollBarDefaultDelayBeforeFade = ViewConfiguration.getScrollDefaultDelay();\n            scrollBarFadeDuration = ViewConfiguration.getScrollBarFadeDuration();\n            this.host = host;\n        }\n\n        public void run() {\n            long now = AnimationUtils.currentAnimationTimeMillis();\n            if (now >= fadeStartTime) {\n\n                // the animation fades the scrollbars out by changing\n                // the opacity (alpha) from fully opaque to fully\n                // transparent\n                int nextFrame = (int) now;\n                int framesCount = 0;\n\n                Interpolator interpolator = scrollBarInterpolator;\n\n                // Start opaque\n                interpolator.setKeyFrame(framesCount++, nextFrame, OPAQUE);\n\n                // End transparent\n                nextFrame += scrollBarFadeDuration;\n                interpolator.setKeyFrame(framesCount, nextFrame, TRANSPARENT);\n\n                state = FADING;\n\n                // Kick off the fade animation\n                // host.invalidate(true);\n                host.invalidate();\n            }\n        }\n    }\n\n    public View getView() {\n        return mView;\n    }\n\n    private int dp2px(float dp) {\n        return (int) (mDensity * dp + 0.5f);\n    }\n\n    public void setIndicatorText(String indicator) {\n        if (mIndicatorPopup != null) {\n            mIndicatorPopup.setIndicatorText(indicator);\n        }\n    }\n\n    public void initIndicatorPopup(IndicatorPopup indicatorPopup) {\n        mIndicatorPopup = indicatorPopup;\n    }\n\n    // ===========================================================\n    // IndicatorPopup\n    // ===========================================================\n    public static class IndicatorPopup {\n\n        public static class Builder {\n\n            private final float density;\n            private final View anchor;\n            private int indicatorPopupColor = COLOR_THUMB_PRESSED;\n            private int indicatorPopupSize;\n            private int indicatorTextSize;\n            private int indicatorMarginRight;\n            private int indicatorPopupAnimationStyle = FASTSCROLLER_INDICATOR_POPUPANIMATIONSTYLE;\n\n            public Builder(FastScrollDelegate delegate) {\n                this.anchor = delegate.getView();\n                this.density = anchor.getContext().getResources().getDisplayMetrics().density;\n                indicatorPopupSize = dp2px(FASTSCROLLER_INDICATOR_SIZE_DP);\n                indicatorTextSize = dp2px(FASTSCROLLER_INDICATOR_TEXTSIZE_DP);\n                indicatorMarginRight = dp2px(FASTSCROLLER_INDICATOR_MARINRIGHT_DP);\n            }\n\n            public Builder indicatorPopupColor(int popupColor) {\n                indicatorPopupColor = popupColor;\n                return this;\n            }\n\n            public Builder indicatorPopupSize(int popupSizeDp) {\n                indicatorPopupSize = dp2px(popupSizeDp);\n                return this;\n            }\n\n            public Builder indicatorTextSize(int textSizeDp) {\n                indicatorTextSize = dp2px(textSizeDp);\n                return this;\n            }\n\n            public Builder indicatorMarginRight(int marginRightDp) {\n                indicatorMarginRight = dp2px(marginRightDp);\n                return this;\n            }\n\n            public Builder indicatorPopupAnimationStyle(int animationStyle) {\n                indicatorPopupAnimationStyle = animationStyle;\n                return this;\n            }\n\n            private int dp2px(float dp) {\n                return (int) (density * dp + 0.5f);\n            }\n\n            public IndicatorPopup build() {\n                return new IndicatorPopup(anchor, indicatorPopupColor, indicatorPopupSize, indicatorTextSize,\n                        indicatorMarginRight, indicatorPopupAnimationStyle);\n            }\n\n        }\n\n        final View anchor;\n        final int popupSize;\n        final int marginRight;\n        final TextView bubbleView;\n        int xOffset, yOffset;\n        final PopupWindow popupWindow;\n\n        @SuppressWarnings(\"deprecation\")\n        private IndicatorPopup(View anchor, int popupColor, int popupSize, int textSize, int marginRight,\n                               int popupAnimationStyle) {\n            super();\n            this.anchor = anchor;\n            this.popupSize = popupSize;\n            this.marginRight = marginRight;\n            this.bubbleView = new TextView(anchor.getContext());\n            bubbleView.setGravity(Gravity.CENTER);\n            bubbleView.setTextColor(Color.WHITE);\n            bubbleView.setSingleLine();\n            bubbleView.setBackgroundDrawable(new BubbleDrawable(popupColor));\n            bubbleView.setEllipsize(TruncateAt.END);\n            bubbleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);\n            this.popupWindow = new PopupWindow(bubbleView, popupSize, popupSize, false);\n            popupWindow.setAnimationStyle(popupAnimationStyle);\n        }\n\n        public int getPopupSize() {\n            return popupSize;\n        }\n\n        public void setOffset(int xoff, int yoff) {\n            this.xOffset = xoff;\n            this.yOffset = yoff;\n            if (popupWindow != null && popupWindow.isShowing()) {\n                popupWindow.update(anchor, xoff - marginRight, yoff, popupSize, popupSize);\n            }\n\n        }\n\n        public void show() {\n            if (popupWindow != null && !popupWindow.isShowing()) {\n                popupWindow.showAsDropDown(anchor, xOffset - marginRight, yOffset);\n            }\n        }\n\n        public void dismiss() {\n            if (popupWindow != null && popupWindow.isShowing()) {\n                popupWindow.dismiss();\n            }\n        }\n\n        public void setIndicatorText(String indicator) {\n            bubbleView.setText(indicator);\n        }\n    }\n\n    private static class BubbleDrawable extends Drawable {\n\n        private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);\n        private final Path path = new Path();\n        private final RectF rectF = new RectF();\n\n        public BubbleDrawable(int color) {\n            super();\n            paint.setColor(color);\n        }\n\n        @Override\n        public void draw(Canvas canvas) {\n            canvas.drawPath(path, paint);\n        }\n\n        @Override\n        protected void onBoundsChange(Rect bounds) {\n            super.onBoundsChange(bounds);\n            final int w = bounds.width();\n            final int h = bounds.height();\n            path.reset();\n            final float radius = Math.min(w, h) / 2f - 1f;\n            final float cx = w / 2f;\n            final float cy = h / 2f;\n            rectF.set(cx - radius, cy - radius, cx + radius, cy + radius);\n            // path.addArc()//Not work\n            path.arcTo(rectF, 0, -270, true);\n            path.lineTo(cx + radius, cy + radius);\n            path.close();\n        }\n\n        @Override\n        public void setAlpha(int alpha) {\n            paint.setAlpha(alpha);\n        }\n\n        @Override\n        public void setColorFilter(ColorFilter cf) {\n            paint.setColorFilter(cf);\n        }\n\n        @Override\n        public int getOpacity() {\n            return PixelFormat.TRANSLUCENT;\n        }\n\n    }\n\n    // ==================\n    // Builder\n    // ==================\n\n    public static int FASTSCROLLER_WIDTH_DP = 20;// 12;\n    public static int FASTSCROLLER_MIN_HEIGHT_DP = 32;\n    public static int FASTSCROLLER_THUMB_WIDTH = 4;\n    public static int FASTSCROLLER_THUMB_INSET_TOP_BOTTOM_RIGHT = 4;\n\n    public static int COLOR_THUMB_NORMAL = 0x80808080;\n    public static int COLOR_THUMB_PRESSED = 0xff03a9f4;// 0xff45c01a;\n\n    private static int FASTSCROLLER_INDICATOR_SIZE_DP = 72;\n    private static int FASTSCROLLER_INDICATOR_MARINRIGHT_DP = 24;\n    private static int FASTSCROLLER_INDICATOR_TEXTSIZE_DP = 36;\n    private static int FASTSCROLLER_INDICATOR_POPUPANIMATIONSTYLE = android.R.style.Animation_Dialog;\n\n    public static class Builder {\n        private final float density;\n        private final FastScrollable fastScrollable;\n        private int width;\n        private int height;\n        private boolean isDynamicHeight = true;\n        private Drawable thumbDrawable;\n        private int thumbNormalColor = COLOR_THUMB_NORMAL;\n        private int thumbPressedColor = COLOR_THUMB_PRESSED;\n\n        public Builder(FastScrollable fastScrollable) {\n            super();\n            this.fastScrollable = fastScrollable;\n            this.density = fastScrollable.getFastScrollableView().getContext().getResources().getDisplayMetrics().density;\n            width = dp2px(FASTSCROLLER_WIDTH_DP);\n            height = dp2px(FASTSCROLLER_MIN_HEIGHT_DP);\n\n        }\n\n        public Builder width(float widthDp) {\n            width = dp2px(widthDp);\n            return this;\n        }\n\n        public Builder height(float heightDp) {\n            height = dp2px(heightDp);\n            return this;\n        }\n\n        public Builder thumbNormalColor(int normalColor) {\n            thumbNormalColor = normalColor;\n            return this;\n        }\n\n        public Builder thumbPressedColor(int pressedColor) {\n            thumbPressedColor = pressedColor;\n            return this;\n        }\n\n        public Builder thumbDrawable(Drawable thumb) {\n            thumbDrawable = thumb;\n            return this;\n        }\n\n        public Builder dynamicHeight(boolean isDynamic) {\n            isDynamicHeight = isDynamic;\n            return this;\n        }\n\n        public FastScrollDelegate build() {\n            if (this.thumbDrawable == null) {\n                this.thumbDrawable = makeDefaultThumbDrawable();\n            }\n            return new FastScrollDelegate(fastScrollable, width, height, thumbDrawable, isDynamicHeight);\n        }\n\n        private Drawable makeDefaultThumbDrawable() {\n            StateListDrawable stateListDrawable = new StateListDrawable();\n            GradientDrawable pressedDrawable = new GradientDrawable();\n            pressedDrawable.setColor(thumbPressedColor);\n            final float radius = width / 2f;\n            final int inset = dp2px(FASTSCROLLER_THUMB_INSET_TOP_BOTTOM_RIGHT);// inset\n            final int insetLeft = width - inset - dp2px(FASTSCROLLER_THUMB_WIDTH);\n            pressedDrawable.setCornerRadius(radius);\n            stateListDrawable.addState(DRAWABLE_STATE_PRESSED, new InsetDrawable(pressedDrawable, insetLeft, inset,\n                    inset, inset));\n            GradientDrawable normalDrawable = new GradientDrawable();\n            normalDrawable.setColor(thumbNormalColor);\n            normalDrawable.setCornerRadius(radius);\n            stateListDrawable.addState(DRAWABLE_STATE_DEFAULT, new InsetDrawable(normalDrawable, insetLeft, inset,\n                    inset, inset));\n            return stateListDrawable;\n        }\n\n        public int dp2px(float dp) {\n            return (int) (dp * density + 0.5f);\n        }\n    }\n}\n"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/listview/FastScrollRecyclerView.java",
    "content": "package com.yanzhenjie.recyclerview.listview;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.View;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * https://github.com/Mixiaoxiao/FastScroll-Everywhere FastScrollRecyclerView\n *\n * @author Mixiaoxiao 2016-08-31\n */\npublic class FastScrollRecyclerView extends RecyclerView implements FastScrollDelegate.FastScrollable {\n\n    private FastScrollDelegate mFastScrollDelegate;\n\n    // ===========================================================\n    // Constructors\n    // ===========================================================\n\n    public FastScrollRecyclerView(Context context) {\n        super(context);\n        createFastScrollDelegate(context);\n    }\n\n    public FastScrollRecyclerView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        createFastScrollDelegate(context);\n    }\n\n    public FastScrollRecyclerView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n        createFastScrollDelegate(context);\n    }\n\n    // ===========================================================\n    // createFastScrollDelegate\n    // ===========================================================\n\n    private void createFastScrollDelegate(Context context) {\n        mFastScrollDelegate = new FastScrollDelegate.Builder(this).build();\n    }\n\n    // ===========================================================\n    // Delegate\n    // ===========================================================\n\n    @Override\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n        if (mFastScrollDelegate.onInterceptTouchEvent(ev)) {\n            return true;\n        }\n        return super.onInterceptTouchEvent(ev);\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        if (mFastScrollDelegate.onTouchEvent(event)) {\n            return true;\n        }\n        return super.onTouchEvent(event);\n    }\n\n    @Override\n    protected void onAttachedToWindow() {\n        super.onAttachedToWindow();\n        mFastScrollDelegate.onAttachedToWindow();\n    }\n\n    @Override\n    protected void onVisibilityChanged(View changedView, int visibility) {\n        super.onVisibilityChanged(changedView, visibility);\n        if (mFastScrollDelegate != null) {\n            mFastScrollDelegate.onVisibilityChanged(changedView, visibility);\n        }\n    }\n\n    @Override\n    protected void onWindowVisibilityChanged(int visibility) {\n        super.onWindowVisibilityChanged(visibility);\n        mFastScrollDelegate.onWindowVisibilityChanged(visibility);\n    }\n\n    @Override\n    protected boolean awakenScrollBars() {\n        return mFastScrollDelegate.awakenScrollBars();\n    }\n\n    @Override\n    protected void dispatchDraw(Canvas canvas) {\n        super.dispatchDraw(canvas);\n        mFastScrollDelegate.dispatchDrawOver(canvas);\n    }\n\n    // ===========================================================\n    // FastScrollable IMPL, ViewInternalSuperMethods\n    // ===========================================================\n\n    @Override\n    public void superOnTouchEvent(MotionEvent event) {\n        super.onTouchEvent(event);\n    }\n\n    @Override\n    public int superComputeVerticalScrollExtent() {\n        return super.computeVerticalScrollExtent();\n    }\n\n    @Override\n    public int superComputeVerticalScrollOffset() {\n        return super.computeVerticalScrollOffset();\n    }\n\n    @Override\n    public int superComputeVerticalScrollRange() {\n        return super.computeVerticalScrollRange();\n    }\n\n    @Override\n    public View getFastScrollableView() {\n        return this;\n    }\n\n    /**\n     * @deprecated use {@link #getFastScrollDelegate()} instead\n     */\n    public FastScrollDelegate getDelegate() {\n        return getFastScrollDelegate();\n    }\n\n    @Override\n    public FastScrollDelegate getFastScrollDelegate() {\n        return mFastScrollDelegate;\n    }\n\n    @Override\n    public void setNewFastScrollDelegate(FastScrollDelegate newDelegate) {\n        if (newDelegate == null) {\n            throw new IllegalArgumentException(\"setNewFastScrollDelegate must NOT be NULL.\");\n        }\n        mFastScrollDelegate.onDetachedFromWindow();\n        mFastScrollDelegate = newDelegate;\n        newDelegate.onAttachedToWindow();\n    }\n\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/touch/DefaultItemTouchHelper.java",
    "content": "/*\n * Copyright 2016 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview.touch;\n\nimport androidx.recyclerview.widget.ItemTouchHelper;\n\n/**\n * Created by Yolanda on 2016/4/19.\n */\npublic class DefaultItemTouchHelper extends ItemTouchHelper {\n\n    private ItemTouchHelperCallback mItemTouchHelperCallback;\n\n    /**\n     * Create default item touch helper.\n     */\n    public DefaultItemTouchHelper() {\n        this(new ItemTouchHelperCallback());\n    }\n\n    /**\n     * @param callback the behavior of ItemTouchHelper.\n     */\n    private DefaultItemTouchHelper(ItemTouchHelperCallback callback) {\n        super(callback);\n        mItemTouchHelperCallback = callback;\n    }\n\n    /**\n     * Set OnItemMoveListener.\n     *\n     * @param onItemMoveListener {@link OnItemMoveListener}.\n     */\n    public void setOnItemMoveListener(OnItemMoveListener onItemMoveListener) {\n        mItemTouchHelperCallback.setOnItemMoveListener(onItemMoveListener);\n    }\n\n    /**\n     * Get OnItemMoveListener.\n     *\n     * @return {@link OnItemMoveListener}.\n     */\n    public OnItemMoveListener getOnItemMoveListener() {\n        return mItemTouchHelperCallback.getOnItemMoveListener();\n    }\n\n    /**\n     * Set OnItemMovementListener.\n     *\n     * @param onItemMovementListener {@link OnItemMovementListener}.\n     */\n    public void setOnItemMovementListener(OnItemMovementListener onItemMovementListener) {\n        mItemTouchHelperCallback.setOnItemMovementListener(onItemMovementListener);\n    }\n\n    /**\n     * Get OnItemMovementListener.\n     *\n     * @return {@link OnItemMovementListener}.\n     */\n    public OnItemMovementListener getOnItemMovementListener() {\n        return mItemTouchHelperCallback.getOnItemMovementListener();\n    }\n\n    /**\n     * Set can long press drag.\n     *\n     * @param canDrag drag true, otherwise is can't.\n     */\n    public void setLongPressDragEnabled(boolean canDrag) {\n        mItemTouchHelperCallback.setLongPressDragEnabled(canDrag);\n    }\n\n    /**\n     * Get can long press drag.\n     *\n     * @return drag true, otherwise is can't.\n     */\n    public boolean isLongPressDragEnabled() {\n        return mItemTouchHelperCallback.isLongPressDragEnabled();\n    }\n\n    /**\n     * Set can long press swipe.\n     *\n     * @param canSwipe swipe true, otherwise is can't.\n     */\n    public void setItemViewSwipeEnabled(boolean canSwipe) {\n        mItemTouchHelperCallback.setItemViewSwipeEnabled(canSwipe);\n    }\n\n    /**\n     * Get can long press swipe.\n     *\n     * @return swipe true, otherwise is can't.\n     */\n    public boolean isItemViewSwipeEnabled() {\n        return this.mItemTouchHelperCallback.isItemViewSwipeEnabled();\n    }\n\n    /**\n     * Set OnItemStateChangedListener.\n     *\n     * @param onItemStateChangedListener {@link OnItemStateChangedListener}.\n     */\n    public void setOnItemStateChangedListener(OnItemStateChangedListener onItemStateChangedListener) {\n        this.mItemTouchHelperCallback.setOnItemStateChangedListener(onItemStateChangedListener);\n    }\n\n    /**\n     * Get OnItemStateChangedListener.\n     *\n     * @return {@link OnItemStateChangedListener}.\n     */\n    public OnItemStateChangedListener getOnItemStateChangedListener() {\n        return this.mItemTouchHelperCallback.getOnItemStateChangedListener();\n    }\n\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/touch/ItemTouchHelperCallback.java",
    "content": "/*\n * Copyright 2016 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview.touch;\n\nimport android.graphics.Canvas;\n\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.ItemTouchHelper;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * Created by Yolanda on 2016/4/19.\n */\nclass ItemTouchHelperCallback extends ItemTouchHelper.Callback {\n\n    private OnItemMovementListener onItemMovementListener;\n\n    private OnItemMoveListener onItemMoveListener;\n\n    private OnItemStateChangedListener onItemStateChangedListener;\n\n    private boolean isItemViewSwipeEnabled;\n\n    private boolean isLongPressDragEnabled;\n\n    public ItemTouchHelperCallback() {\n    }\n\n    public void setLongPressDragEnabled(boolean canDrag) {\n        this.isLongPressDragEnabled = canDrag;\n    }\n\n    @Override\n    public boolean isLongPressDragEnabled() {\n        return isLongPressDragEnabled;\n    }\n\n    public void setItemViewSwipeEnabled(boolean canSwipe) {\n        this.isItemViewSwipeEnabled = canSwipe;\n    }\n\n    @Override\n    public boolean isItemViewSwipeEnabled() {\n        return isItemViewSwipeEnabled;\n    }\n\n    public void setOnItemMoveListener(OnItemMoveListener onItemMoveListener) {\n        this.onItemMoveListener = onItemMoveListener;\n    }\n\n    public OnItemMoveListener getOnItemMoveListener() {\n        return onItemMoveListener;\n    }\n\n    public void setOnItemMovementListener(OnItemMovementListener onItemMovementListener) {\n        this.onItemMovementListener = onItemMovementListener;\n    }\n\n    public OnItemMovementListener getOnItemMovementListener() {\n        return onItemMovementListener;\n    }\n\n    public void setOnItemStateChangedListener(OnItemStateChangedListener onItemStateChangedListener) {\n        this.onItemStateChangedListener = onItemStateChangedListener;\n    }\n\n    public OnItemStateChangedListener getOnItemStateChangedListener() {\n        return onItemStateChangedListener;\n    }\n\n    @Override\n    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder targetViewHolder) {\n        if (onItemMovementListener != null) {\n            int dragFlags = onItemMovementListener.onDragFlags(recyclerView, targetViewHolder);\n            int swipeFlags = onItemMovementListener.onSwipeFlags(recyclerView, targetViewHolder);\n            return makeMovementFlags(dragFlags, swipeFlags);\n        } else {\n            RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();\n            if (layoutManager instanceof GridLayoutManager) {\n                LinearLayoutManager linearLayoutManager = (LinearLayoutManager)layoutManager;\n                if (linearLayoutManager.getOrientation() == LinearLayoutManager.HORIZONTAL) {\n                    int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT |\n                        ItemTouchHelper.RIGHT;\n                    int swipeFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;\n                    return makeMovementFlags(dragFlags, swipeFlags);\n                } else {\n                    int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT |\n                        ItemTouchHelper.RIGHT;\n                    int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;\n                    return makeMovementFlags(dragFlags, swipeFlags);\n                }\n            } else if (layoutManager instanceof LinearLayoutManager) {\n                LinearLayoutManager linearLayoutManager = (LinearLayoutManager)layoutManager;\n                if (linearLayoutManager.getOrientation() == LinearLayoutManager.HORIZONTAL) {\n                    int dragFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;\n                    int swipeFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;\n                    return makeMovementFlags(dragFlags, swipeFlags);\n                } else {\n                    int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;\n                    int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;\n                    return makeMovementFlags(dragFlags, swipeFlags);\n                }\n            }\n        }\n        return makeMovementFlags(0, 0);\n    }\n\n    @Override\n    public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY,\n        int actionState, boolean isCurrentlyActive) {\n        // 判断当前是否是swipe方式：侧滑。\n        if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {\n            //1.ItemView--ViewHolder; 2.侧滑条目的透明度程度关联谁？dX(delta增量，范围：当前条目-width~width)。\n            RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();\n            float alpha = 1;\n            if (layoutManager instanceof LinearLayoutManager) {\n                int orientation = ((LinearLayoutManager)layoutManager).getOrientation();\n                if (orientation == LinearLayoutManager.HORIZONTAL) {\n                    alpha = 1 - Math.abs(dY) / viewHolder.itemView.getHeight();\n                } else if (orientation == LinearLayoutManager.VERTICAL) {\n                    alpha = 1 - Math.abs(dX) / viewHolder.itemView.getWidth();\n                }\n            }\n            viewHolder.itemView.setAlpha(alpha);//1~0\n        }\n        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);\n    }\n\n\n    @Override\n    public boolean onMove(RecyclerView arg0, RecyclerView.ViewHolder srcHolder, RecyclerView.ViewHolder targetHolder) {\n        if (onItemMoveListener != null) {\n            // 回调刷新数据及界面。\n            return onItemMoveListener.onItemMove(srcHolder, targetHolder);\n        }\n        return false;\n    }\n\n    @Override\n    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {\n        // 回调刷新数据及界面。\n        if (onItemMoveListener != null) onItemMoveListener.onItemDismiss(viewHolder);\n    }\n\n    @Override\n    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {\n        super.onSelectedChanged(viewHolder, actionState);\n        if (onItemStateChangedListener != null && actionState != OnItemStateChangedListener.ACTION_STATE_IDLE) {\n            onItemStateChangedListener.onSelectedChanged(viewHolder, actionState);\n        }\n    }\n\n    @Override\n    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {\n        super.clearView(recyclerView, viewHolder);\n        if (onItemStateChangedListener != null) {\n            onItemStateChangedListener.onSelectedChanged(viewHolder, OnItemStateChangedListener.ACTION_STATE_IDLE);\n        }\n    }\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/touch/OnItemMoveListener.java",
    "content": "/*\n * Copyright 2016 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview.touch;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * Created by Yolanda on 2016/4/19.\n */\npublic interface OnItemMoveListener {\n\n    /**\n     * When drag and drop the callback.\n     *\n     * @param srcHolder src.\n     * @param targetHolder target.\n     *\n     * @return To deal with the returns true, false otherwise.\n     */\n    boolean onItemMove(RecyclerView.ViewHolder srcHolder, RecyclerView.ViewHolder targetHolder);\n\n    /**\n     * When items should be removed when the callback.\n     *\n     * @param srcHolder src.\n     */\n    void onItemDismiss(RecyclerView.ViewHolder srcHolder);\n\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/touch/OnItemMovementListener.java",
    "content": "/*\n * Copyright 2016 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview.touch;\n\nimport androidx.recyclerview.widget.ItemTouchHelper;\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * Created by Yan Zhenjie on 2016/8/1.\n */\npublic interface OnItemMovementListener {\n\n    int INVALID = 0;\n\n    int LEFT = ItemTouchHelper.LEFT;\n\n    int UP = ItemTouchHelper.UP;\n\n    int RIGHT = ItemTouchHelper.RIGHT;\n\n    int DOWN = ItemTouchHelper.DOWN;\n\n    /**\n     * Can drag and drop the ViewHolder?\n     *\n     * @param recyclerView {@link RecyclerView}.\n     * @param targetViewHolder target ViewHolder.\n     *\n     * @return use {@link #LEFT}, {@link #UP}, {@link #RIGHT}, {@link #DOWN}.\n     */\n    int onDragFlags(RecyclerView recyclerView, RecyclerView.ViewHolder targetViewHolder);\n\n    /**\n     * Can swipe and drop the ViewHolder?\n     *\n     * @param recyclerView {@link RecyclerView}.\n     * @param targetViewHolder target ViewHolder.\n     *\n     * @return use {@link #LEFT}, {@link #UP}, {@link #RIGHT}, {@link #DOWN}.\n     */\n    int onSwipeFlags(RecyclerView recyclerView, RecyclerView.ViewHolder targetViewHolder);\n\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/touch/OnItemStateChangedListener.java",
    "content": "/*\n * Copyright 2016 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview.touch;\n\nimport androidx.recyclerview.widget.ItemTouchHelper;\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * Created by Yan Zhenjie on 2016/8/12.\n */\npublic interface OnItemStateChangedListener {\n\n    /**\n     * ItemTouchHelper is in idle state. At this state, either there is no related motion event by the user or latest\n     * motion events have not yet triggered a swipe or drag.\n     */\n    int ACTION_STATE_IDLE = ItemTouchHelper.ACTION_STATE_IDLE;\n\n    /**\n     * A View is currently being swiped.\n     */\n    int ACTION_STATE_SWIPE = ItemTouchHelper.ACTION_STATE_SWIPE;\n\n    /**\n     * A View is currently being dragged.\n     */\n    int ACTION_STATE_DRAG = ItemTouchHelper.ACTION_STATE_DRAG;\n\n    /**\n     * Called when the ViewHolder swiped or dragged by the ItemTouchHelper is changed.\n     *\n     * @param viewHolder The new ViewHolder that is being swiped or dragged. Might be null if it is cleared.\n     * @param actionState One of {@link OnItemStateChangedListener#ACTION_STATE_IDLE}, {@link\n     *     OnItemStateChangedListener#ACTION_STATE_SWIPE} or {@link OnItemStateChangedListener#ACTION_STATE_DRAG}.\n     */\n    void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState);\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/widget/BorderItemDecoration.java",
    "content": "/*\n * Copyright 2019 Zhenjie Yan\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview.widget;\n\nimport android.graphics.Canvas;\nimport android.graphics.Rect;\nimport android.view.View;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * Created by Zhenjie Yan on 1/30/19.\n */\npublic class BorderItemDecoration extends RecyclerView.ItemDecoration {\n\n    private final int mWidth;\n    private final int mHeight;\n    private final Drawer mDrawer;\n\n    /**\n     * @param color divider line color.\n     */\n    public BorderItemDecoration(@ColorInt int color) {\n        this(color, 4, 4);\n    }\n\n    /**\n     * @param color line color.\n     * @param width line width.\n     * @param height line height.\n     */\n    public BorderItemDecoration(@ColorInt int color, int width, int height) {\n        this.mWidth = Math.round(width / 2F);\n        this.mHeight = Math.round(height / 2F);\n        this.mDrawer = new ColorDrawer(color, mWidth, mHeight);\n    }\n\n    @Override\n    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent,\n        @NonNull RecyclerView.State state) {\n        outRect.set(mWidth, mHeight, mWidth, mHeight);\n    }\n\n    @Override\n    public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {\n        canvas.save();\n        RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();\n        assert layoutManager != null;\n        int childCount = layoutManager.getChildCount();\n        for (int i = 0; i < childCount; i++) {\n            final View view = layoutManager.getChildAt(i);\n            mDrawer.drawLeft(view, canvas);\n            mDrawer.drawTop(view, canvas);\n            mDrawer.drawRight(view, canvas);\n            mDrawer.drawBottom(view, canvas);\n        }\n        canvas.restore();\n    }\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/widget/ColorDrawer.java",
    "content": "/*\n * Copyright 2018 Yan Zhenjie.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview.widget;\n\nimport android.graphics.Color;\nimport android.graphics.drawable.ColorDrawable;\n\nimport androidx.annotation.ColorInt;\n\n/**\n * Created by YanZhenjie on 2018/4/20.\n */\nclass ColorDrawer extends Drawer {\n\n    public ColorDrawer(int color, int width, int height) {\n        super(new ColorDrawable(opaqueColor(color)), width, height);\n    }\n\n    /**\n     * The target color is packaged in an opaque color.\n     *\n     * @param color color.\n     *\n     * @return color.\n     */\n    @ColorInt\n    public static int opaqueColor(@ColorInt int color) {\n        int alpha = Color.alpha(color);\n        if (alpha == 0) return color;\n        int red = Color.red(color);\n        int green = Color.green(color);\n        int blue = Color.blue(color);\n        return Color.argb(255, red, green, blue);\n    }\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/widget/DefaultItemDecoration.java",
    "content": "/*\n * Copyright 2017 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview.widget;\n\nimport android.graphics.Canvas;\nimport android.graphics.Rect;\nimport android.view.View;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.StaggeredGridLayoutManager;\n\n/**\n * Created by YanZhenjie on 2017/8/14.\n */\npublic class DefaultItemDecoration extends RecyclerView.ItemDecoration {\n\n    private final int mWidth;\n    private final int mHeight;\n    private final Drawer mDrawer;\n\n    /**\n     * @param color divider line color.\n     */\n    public DefaultItemDecoration(@ColorInt int color) {\n        this(color, 4, 4);\n    }\n\n    /**\n     * @param color line color.\n     * @param width line width.\n     * @param height line height.\n     */\n    public DefaultItemDecoration(@ColorInt int color, int width, int height) {\n        this.mWidth = Math.round(width / 2F);\n        this.mHeight = Math.round(height / 2F);\n        this.mDrawer = new ColorDrawer(color, mWidth, mHeight);\n    }\n\n    @Override\n    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent,\n        @NonNull RecyclerView.State state) {\n        RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();\n        if (layoutManager instanceof LinearLayoutManager) {\n            int orientation = getOrientation(layoutManager);\n            int position = parent.getChildLayoutPosition(view);\n            int spanCount = getSpanCount(layoutManager);\n            int childCount = layoutManager.getItemCount();\n\n            if (orientation == RecyclerView.VERTICAL) {\n                offsetVertical(outRect, position, spanCount, childCount);\n            } else {\n                offsetHorizontal(outRect, position, spanCount, childCount);\n            }\n        } else if (layoutManager instanceof StaggeredGridLayoutManager) {\n            outRect.set(mWidth, mHeight, mWidth, mHeight); // |-|-\n        }\n    }\n\n    private void offsetHorizontal(Rect outRect, int position, int spanCount, int childCount) {\n        boolean firstRaw = isFirstRaw(RecyclerView.HORIZONTAL, position, spanCount, childCount);\n        boolean lastRaw = isLastRaw(RecyclerView.HORIZONTAL, position, spanCount, childCount);\n        boolean firstColumn = isFirstColumn(RecyclerView.HORIZONTAL, position, spanCount, childCount);\n        boolean lastColumn = isLastColumn(RecyclerView.HORIZONTAL, position, spanCount, childCount);\n\n        if (spanCount == 1) {\n            if (firstColumn && lastColumn) { // xxxx\n                outRect.set(0, 0, 0, 0);\n            } else if (firstColumn) { // xx|x\n                outRect.set(0, 0, mWidth, 0);\n            } else if (lastColumn) { // |xxx\n                outRect.set(mWidth, 0, 0, 0);\n            } else { // |x|x\n                outRect.set(mWidth, 0, mWidth, 0);\n            }\n        } else {\n            if (firstColumn && firstRaw) { // xx|-\n                outRect.set(0, 0, mWidth, mHeight);\n            } else if (firstColumn && lastRaw) { // x-|x\n                outRect.set(0, mHeight, mWidth, 0);\n            } else if (lastColumn && firstRaw) { // |xx-\n                outRect.set(mWidth, 0, 0, mHeight);\n            } else if (lastColumn && lastRaw) { // |-xx\n                outRect.set(mWidth, mHeight, 0, 0);\n            } else if (firstColumn) { // x-|-\n                outRect.set(0, mHeight, mWidth, mHeight);\n            } else if (lastColumn) { // |-x-\n                outRect.set(mWidth, mHeight, 0, mHeight);\n            } else if (firstRaw) { // |x|-\n                outRect.set(mWidth, 0, mWidth, mHeight);\n            } else if (lastRaw) { // |-|x\n                outRect.set(mWidth, mHeight, mWidth, 0);\n            } else { // |-|-\n                outRect.set(mWidth, mHeight, mWidth, mHeight);\n            }\n        }\n    }\n\n    private void offsetVertical(Rect outRect, int position, int spanCount, int childCount) {\n        boolean firstRaw = isFirstRaw(RecyclerView.VERTICAL, position, spanCount, childCount);\n        boolean lastRaw = isLastRaw(RecyclerView.VERTICAL, position, spanCount, childCount);\n        boolean firstColumn = isFirstColumn(RecyclerView.VERTICAL, position, spanCount, childCount);\n        boolean lastColumn = isLastColumn(RecyclerView.VERTICAL, position, spanCount, childCount);\n\n        if (spanCount == 1) {\n            if (firstRaw && lastRaw) { // xxxx\n                outRect.set(0, 0, 0, 0);\n            } else if (firstRaw) { // xxx-\n                outRect.set(0, 0, 0, mHeight);\n            } else if (lastRaw) { // x-xx\n                outRect.set(0, mHeight, 0, 0);\n            } else { // x-x-\n                outRect.set(0, mHeight, 0, mHeight);\n            }\n        } else {\n            if (firstRaw && firstColumn) { // xx|-\n                outRect.set(0, 0, mWidth, mHeight);\n            } else if (firstRaw && lastColumn) { // |xx-\n                outRect.set(mWidth, 0, 0, mHeight);\n            } else if (lastRaw && firstColumn) { // x-|x\n                outRect.set(0, mHeight, mWidth, 0);\n            } else if (lastRaw && lastColumn) { // |-xx\n                outRect.set(mWidth, mHeight, 0, 0);\n            } else if (firstRaw) { // |x|-\n                outRect.set(mWidth, 0, mWidth, mHeight);\n            } else if (lastRaw) { // |-|x\n                outRect.set(mWidth, mHeight, mWidth, 0);\n            } else if (firstColumn) { // x-|-\n                outRect.set(0, mHeight, mWidth, mHeight);\n            } else if (lastColumn) { // |-x-\n                outRect.set(mWidth, mHeight, 0, mHeight);\n            } else { // |-|-\n                outRect.set(mWidth, mHeight, mWidth, mHeight);\n            }\n        }\n    }\n\n    private int getOrientation(RecyclerView.LayoutManager layoutManager) {\n        if (layoutManager instanceof LinearLayoutManager) {\n            return ((LinearLayoutManager)layoutManager).getOrientation();\n        } else if (layoutManager instanceof StaggeredGridLayoutManager) {\n            return ((StaggeredGridLayoutManager)layoutManager).getOrientation();\n        }\n        return RecyclerView.VERTICAL;\n    }\n\n    private int getSpanCount(RecyclerView.LayoutManager layoutManager) {\n        if (layoutManager instanceof GridLayoutManager) {\n            return ((GridLayoutManager)layoutManager).getSpanCount();\n        } else if (layoutManager instanceof StaggeredGridLayoutManager) {\n            return ((StaggeredGridLayoutManager)layoutManager).getSpanCount();\n        }\n        return 1;\n    }\n\n    private boolean isFirstRaw(int orientation, int position, int columnCount, int childCount) {\n        if (orientation == RecyclerView.VERTICAL) {\n            return position < columnCount;\n        } else {\n            if (columnCount == 1) return true;\n            return position % columnCount == 0;\n        }\n    }\n\n    private boolean isLastRaw(int orientation, int position, int columnCount, int childCount) {\n        if (orientation == RecyclerView.VERTICAL) {\n            if (columnCount == 1) {\n                return position + 1 == childCount;\n            } else {\n                int lastRawItemCount = childCount % columnCount;\n                int rawCount = (childCount - lastRawItemCount) / columnCount + (lastRawItemCount > 0 ? 1 : 0);\n\n                int rawPositionJudge = (position + 1) % columnCount;\n                if (rawPositionJudge == 0) {\n                    int positionRaw = (position + 1) / columnCount;\n                    return rawCount == positionRaw;\n                } else {\n                    int rawPosition = (position + 1 - rawPositionJudge) / columnCount + 1;\n                    return rawCount == rawPosition;\n                }\n            }\n        } else {\n            if (columnCount == 1) return true;\n            return (position + 1) % columnCount == 0;\n        }\n    }\n\n    private boolean isFirstColumn(int orientation, int position, int columnCount, int childCount) {\n        if (orientation == RecyclerView.VERTICAL) {\n            if (columnCount == 1) return true;\n            return position % columnCount == 0;\n        } else {\n            return position < columnCount;\n        }\n    }\n\n    private boolean isLastColumn(int orientation, int position, int columnCount, int childCount) {\n        if (orientation == RecyclerView.VERTICAL) {\n            if (columnCount == 1) return true;\n            return (position + 1) % columnCount == 0;\n        } else {\n            if (columnCount == 1) {\n                return position + 1 == childCount;\n            } else {\n                int lastRawItemCount = childCount % columnCount;\n                int rawCount = (childCount - lastRawItemCount) / columnCount + (lastRawItemCount > 0 ? 1 : 0);\n\n                int rawPositionJudge = (position + 1) % columnCount;\n                if (rawPositionJudge == 0) {\n                    int positionRaw = (position + 1) / columnCount;\n                    return rawCount == positionRaw;\n                } else {\n                    int rawPosition = (position + 1 - rawPositionJudge) / columnCount + 1;\n                    return rawCount == rawPosition;\n                }\n            }\n        }\n    }\n\n    @Override\n    public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {\n        RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();\n        assert layoutManager != null;\n        int orientation = getOrientation(layoutManager);\n        int spanCount = getSpanCount(layoutManager);\n        int childCount = layoutManager.getChildCount();\n\n        if (layoutManager instanceof LinearLayoutManager) {\n            canvas.save();\n            for (int i = 0; i < childCount; i++) {\n                View view = layoutManager.getChildAt(i);\n                assert view != null;\n                int position = parent.getChildLayoutPosition(view);\n\n                if (orientation == RecyclerView.VERTICAL) {\n                    drawVertical(canvas, view, position, spanCount, childCount);\n                } else {\n                    drawHorizontal(canvas, view, position, spanCount, childCount);\n                }\n            }\n            canvas.restore();\n        } else if (layoutManager instanceof StaggeredGridLayoutManager) {\n            canvas.save();\n            for (int i = 0; i < childCount; i++) {\n                View view = layoutManager.getChildAt(i);\n                mDrawer.drawLeft(view, canvas);\n                mDrawer.drawTop(view, canvas);\n                mDrawer.drawRight(view, canvas);\n                mDrawer.drawBottom(view, canvas);\n            }\n            canvas.restore();\n        }\n    }\n\n    private void drawHorizontal(Canvas canvas, View view, int position, int spanCount, int childCount) {\n        boolean firstRaw = isFirstRaw(RecyclerView.HORIZONTAL, position, spanCount, childCount);\n        boolean lastRaw = isLastRaw(RecyclerView.HORIZONTAL, position, spanCount, childCount);\n        boolean firstColumn = isFirstColumn(RecyclerView.HORIZONTAL, position, spanCount, childCount);\n        boolean lastColumn = isLastColumn(RecyclerView.HORIZONTAL, position, spanCount, childCount);\n\n        if (spanCount == 1) {\n            if (firstRaw && lastColumn) { // xxxx\n                // Nothing.\n            } else if (firstColumn) { // xx|x\n                mDrawer.drawRight(view, canvas);\n            } else if (lastColumn) { // |xxx\n                mDrawer.drawLeft(view, canvas);\n            } else { // |x|x\n                mDrawer.drawLeft(view, canvas);\n                mDrawer.drawRight(view, canvas);\n            }\n        } else {\n            if (firstColumn && firstRaw) { // xx|-\n                mDrawer.drawRight(view, canvas);\n                mDrawer.drawBottom(view, canvas);\n            } else if (firstColumn && lastRaw) { // x-|x\n                mDrawer.drawTop(view, canvas);\n                mDrawer.drawRight(view, canvas);\n            } else if (lastColumn && firstRaw) { // |xx-\n                mDrawer.drawLeft(view, canvas);\n                mDrawer.drawBottom(view, canvas);\n            } else if (lastColumn && lastRaw) { // |-xx\n                mDrawer.drawLeft(view, canvas);\n                mDrawer.drawTop(view, canvas);\n            } else if (firstColumn) { // x-|-\n                mDrawer.drawTop(view, canvas);\n                mDrawer.drawRight(view, canvas);\n                mDrawer.drawBottom(view, canvas);\n            } else if (lastColumn) { // |-x-\n                mDrawer.drawLeft(view, canvas);\n                mDrawer.drawTop(view, canvas);\n                mDrawer.drawBottom(view, canvas);\n            } else if (firstRaw) { // |x|-\n                mDrawer.drawLeft(view, canvas);\n                mDrawer.drawRight(view, canvas);\n                mDrawer.drawBottom(view, canvas);\n            } else if (lastRaw) { // |-|x\n                mDrawer.drawLeft(view, canvas);\n                mDrawer.drawTop(view, canvas);\n                mDrawer.drawRight(view, canvas);\n            } else { // |-|-\n                mDrawer.drawLeft(view, canvas);\n                mDrawer.drawTop(view, canvas);\n                mDrawer.drawRight(view, canvas);\n                mDrawer.drawBottom(view, canvas);\n            }\n        }\n    }\n\n    private void drawVertical(Canvas canvas, View view, int position, int spanCount, int childCount) {\n        boolean firstRaw = isFirstRaw(RecyclerView.VERTICAL, position, spanCount, childCount);\n        boolean lastRaw = isLastRaw(RecyclerView.VERTICAL, position, spanCount, childCount);\n        boolean firstColumn = isFirstColumn(RecyclerView.VERTICAL, position, spanCount, childCount);\n        boolean lastColumn = isLastColumn(RecyclerView.VERTICAL, position, spanCount, childCount);\n\n        if (spanCount == 1) {\n            if (firstRaw && lastRaw) { // xxxx\n                // Nothing.\n            } else if (firstRaw) { // xxx-\n                mDrawer.drawBottom(view, canvas);\n            } else if (lastRaw) { // x-xx\n                mDrawer.drawTop(view, canvas);\n            } else { // x-x-\n                mDrawer.drawTop(view, canvas);\n                mDrawer.drawBottom(view, canvas);\n            }\n        } else {\n            if (firstRaw && firstColumn) { // xx|-\n                mDrawer.drawRight(view, canvas);\n                mDrawer.drawBottom(view, canvas);\n            } else if (firstRaw && lastColumn) { // |xx-\n                mDrawer.drawLeft(view, canvas);\n                mDrawer.drawBottom(view, canvas);\n            } else if (lastRaw && firstColumn) { // x-|x\n                mDrawer.drawTop(view, canvas);\n                mDrawer.drawRight(view, canvas);\n            } else if (lastRaw && lastColumn) { // |-xx\n                mDrawer.drawLeft(view, canvas);\n                mDrawer.drawTop(view, canvas);\n            } else if (firstRaw) { // |x|-\n                mDrawer.drawLeft(view, canvas);\n                mDrawer.drawRight(view, canvas);\n                mDrawer.drawBottom(view, canvas);\n            } else if (lastRaw) { // |-|x\n                mDrawer.drawLeft(view, canvas);\n                mDrawer.drawTop(view, canvas);\n                mDrawer.drawRight(view, canvas);\n            } else if (firstColumn) { // x-|-\n                mDrawer.drawTop(view, canvas);\n                mDrawer.drawRight(view, canvas);\n                mDrawer.drawBottom(view, canvas);\n            } else if (lastColumn) { // |-x-\n                mDrawer.drawLeft(view, canvas);\n                mDrawer.drawTop(view, canvas);\n                mDrawer.drawBottom(view, canvas);\n            } else { // |-|-\n                mDrawer.drawLeft(view, canvas);\n                mDrawer.drawTop(view, canvas);\n                mDrawer.drawRight(view, canvas);\n                mDrawer.drawBottom(view, canvas);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/widget/DefaultLoadMoreView.java",
    "content": "/*\n * Copyright 2017 Yan Zhenjie\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview.widget;\n\nimport android.content.Context;\nimport android.text.TextUtils;\nimport android.util.AttributeSet;\nimport android.util.DisplayMetrics;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.LinearLayout;\nimport android.widget.ProgressBar;\nimport android.widget.TextView;\n\nimport androidx.annotation.Nullable;\n\nimport com.yanzhenjie.recyclerview.R;\nimport com.yanzhenjie.recyclerview.SwipeRecyclerView;\n\n/**\n * Created by YanZhenjie on 2017/7/21.\n */\npublic class DefaultLoadMoreView extends LinearLayout implements SwipeRecyclerView.LoadMoreView, View.OnClickListener {\n\n    private ProgressBar mProgressBar;\n    private TextView mTvMessage;\n\n    private SwipeRecyclerView.LoadMoreListener mLoadMoreListener;\n\n    public DefaultLoadMoreView(Context context) {\n        this(context, null);\n    }\n\n    public DefaultLoadMoreView(Context context, @Nullable AttributeSet attrs) {\n        super(context, attrs);\n        setLayoutParams(new ViewGroup.LayoutParams(-1, -2));\n        setGravity(Gravity.CENTER);\n        setVisibility(GONE);\n\n        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();\n\n        int minHeight = (int)(displayMetrics.density * 60 + 0.5);\n        setMinimumHeight(minHeight);\n\n        inflate(getContext(), R.layout.support_recycler_view_load_more, this);\n        mProgressBar = findViewById(R.id.progress_bar);\n        mTvMessage = findViewById(R.id.tv_load_more_message);\n        setOnClickListener(this);\n    }\n\n    @Override\n    public void onLoading() {\n        setVisibility(VISIBLE);\n        mProgressBar.setVisibility(VISIBLE);\n        mTvMessage.setVisibility(VISIBLE);\n        mTvMessage.setText(R.string.support_recycler_load_more_message);\n    }\n\n    @Override\n    public void onLoadFinish(boolean dataEmpty, boolean hasMore) {\n        if (!hasMore) {\n            setVisibility(VISIBLE);\n\n            if (dataEmpty) {\n                mProgressBar.setVisibility(GONE);\n                mTvMessage.setVisibility(VISIBLE);\n                mTvMessage.setText(R.string.support_recycler_data_empty);\n            } else {\n                mProgressBar.setVisibility(GONE);\n                mTvMessage.setVisibility(VISIBLE);\n                mTvMessage.setText(R.string.support_recycler_more_not);\n            }\n        } else {\n            setVisibility(INVISIBLE);\n        }\n    }\n\n    @Override\n    public void onWaitToLoadMore(SwipeRecyclerView.LoadMoreListener loadMoreListener) {\n        this.mLoadMoreListener = loadMoreListener;\n\n        setVisibility(VISIBLE);\n        mProgressBar.setVisibility(GONE);\n        mTvMessage.setVisibility(VISIBLE);\n        mTvMessage.setText(R.string.support_recycler_click_load_more);\n    }\n\n    @Override\n    public void onLoadError(int errorCode, String errorMessage) {\n        setVisibility(VISIBLE);\n        mProgressBar.setVisibility(GONE);\n        mTvMessage.setVisibility(VISIBLE);\n        mTvMessage.setText(TextUtils.isEmpty(errorMessage)\n            ? getContext().getString(R.string.support_recycler_load_error)\n            : errorMessage);\n    }\n\n    @Override\n    public void onClick(View v) {\n        if (mLoadMoreListener != null) mLoadMoreListener.onLoadMore();\n    }\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/widget/Drawer.java",
    "content": "/*\n * Copyright 2018 Yan Zhenjie.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.yanzhenjie.recyclerview.widget;\n\nimport android.graphics.Canvas;\nimport android.graphics.drawable.Drawable;\nimport android.view.View;\n\n/**\n * Created by YanZhenjie on 2018/4/20.\n */\nclass Drawer {\n\n    private final Drawable mDivider;\n    private final int mWidth;\n    private final int mHeight;\n\n    public Drawer(Drawable divider, int width, int height) {\n        this.mDivider = divider;\n        this.mWidth = width;\n        this.mHeight = height;\n    }\n\n    /**\n     * Draw the divider on the left side of the Item.\n     */\n    public void drawLeft(View view, Canvas c) {\n        int left = view.getLeft() - mWidth;\n        int top = view.getTop() - mHeight;\n        int right = left + mWidth;\n        int bottom = view.getBottom() + mHeight;\n        mDivider.setBounds(left, top, right, bottom);\n        mDivider.draw(c);\n    }\n\n    /**\n     * Draw the divider on the top side of the Item.\n     */\n    public void drawTop(View view, Canvas c) {\n        int left = view.getLeft() - mWidth;\n        int top = view.getTop() - mHeight;\n        int right = view.getRight() + mWidth;\n        int bottom = top + mHeight;\n        mDivider.setBounds(left, top, right, bottom);\n        mDivider.draw(c);\n    }\n\n    /**\n     * Draw the divider on the top side of the Item.\n     */\n    public void drawRight(View view, Canvas c) {\n        int left = view.getRight();\n        int top = view.getTop() - mHeight;\n        int right = left + mWidth;\n        int bottom = view.getBottom() + mHeight;\n        mDivider.setBounds(left, top, right, bottom);\n        mDivider.draw(c);\n    }\n\n    /**\n     * Draw the divider on the top side of the Item.\n     */\n    public void drawBottom(View view, Canvas c) {\n        int left = view.getLeft() - mWidth;\n        int top = view.getBottom();\n        int right = view.getRight() + mWidth;\n        int bottom = top + mHeight;\n        mDivider.setBounds(left, top, right, bottom);\n        mDivider.draw(c);\n    }\n}"
  },
  {
    "path": "support/src/main/java/com/yanzhenjie/recyclerview/widget/StickyNestedScrollView.java",
    "content": "///*\n// * Copyright 2017 Yan Zhenjie\n// *\n// * Licensed under the Apache License, Version 2.0 (the \"License\");\n// * you may not use this file except in compliance with the License.\n// * You may obtain a copy of the License at\n// *\n// *      http://www.apache.org/licenses/LICENSE-2.0\n// *\n// * Unless required by applicable law or agreed to in writing, software\n// * distributed under the License is distributed on an \"AS IS\" BASIS,\n// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// * See the License for the specific language governing permissions and\n// * limitations under the License.\n// */\n//package com.yanzhenjie.recyclerview.widget;\n//\n//import android.content.Context;\n//import android.graphics.Canvas;\n//import android.graphics.drawable.Drawable;\n//import android.support.v4.widget.NestedScrollView;\n//import android.util.AttributeSet;\n//import android.util.Log;\n//import android.view.MotionEvent;\n//import android.view.View;\n//import android.view.ViewGroup;\n//\n//import java.util.ArrayList;\n//import java.util.List;\n//\n///**\n// * Created by YanZhenjie on 2017/7/20.\n// */\n//public class StickyNestedScrollView extends NestedScrollView {\n//\n//    public interface OnViewStickyListener {\n//        void onSticky(View view);\n//        void onUnSticky(View view);\n//    }\n//\n//    /**\n//     * Tag for views that should stick and have constant drawing. e.g. TextViews, ImageViews etc\n//     */\n//    public static final String STICKY_TAG = \"sticky\";\n//    /**\n//     * Flag for views that should stick and have non-constant drawing. e.g. Buttons, ProgressBars etc\n//     */\n//    public static final String FLAG_NONCONSTANT = \"-nonconstant\";\n//    /**\n//     * Flag for views that have aren't fully opaque\n//     */\n//    public static final String FLAG_HASTRANSPARENCY = \"-hastransparency\";\n//    /**\n//     * Default height of the shadow peeking out below the stuck view.\n//     */\n//    private static final int DEFAULT_SHADOW_HEIGHT = 10; // dp;\n//    private ArrayList<View> stickyViews;\n//    private View currentlyStickingView;\n//    private float stickyViewTopOffset;\n//    private final Runnable invalidateRunnable = new Runnable() {\n//        @Override\n//        public void run() {\n//            if (currentlyStickingView != null) {\n//                int l = getLeftForViewRelativeOnlyChild(currentlyStickingView);\n//                int t = getBottomForViewRelativeOnlyChild(currentlyStickingView);\n//                int r = getRightForViewRelativeOnlyChild(currentlyStickingView);\n//                int b = (int)(getScrollY() + (currentlyStickingView.getHeight() + stickyViewTopOffset));\n//                invalidate(l, t, r, b);\n//            }\n//            postDelayed(this, 16);\n//        }\n//    };\n//    private int stickyViewLeftOffset;\n//    private boolean redirectTouchesToStickyView;\n//    private boolean clippingToPadding;\n//    private boolean clipToPaddingHasBeenSet;\n//    private int mShadowHeight = DEFAULT_SHADOW_HEIGHT;\n//    private Drawable mShadowDrawable;\n//    private boolean hasNotDoneActionDown = true;\n//\n//    private List<OnViewStickyListener> mOnViewStickyListeners;\n//\n//    public StickyNestedScrollView(Context context) {\n//        this(context, null);\n//    }\n//\n//    public StickyNestedScrollView(Context context, AttributeSet attrs) {\n//        this(context, attrs, android.R.attr.scrollViewStyle);\n//    }\n//\n//    public StickyNestedScrollView(Context context, AttributeSet attrs, int defStyle) {\n//        super(context, attrs, defStyle);\n//        setup();\n//    }\n//\n//    public void addOnViewStickyListener(OnViewStickyListener stickyListener) {\n//        if (mOnViewStickyListeners == null) mOnViewStickyListeners = new ArrayList<>();\n//        mOnViewStickyListeners.add(stickyListener);\n//    }\n//\n//    public void removeOnViewStickyListener(OnViewStickyListener stickyListener) {\n//        if (mOnViewStickyListeners != null) mOnViewStickyListeners.remove(stickyListener);\n//    }\n//\n//    public void clearOnViewStickyListener() {\n//        if (mOnViewStickyListeners != null) mOnViewStickyListeners.clear();\n//    }\n//\n//    public void setShadowHeight(int height) {\n//        mShadowHeight = height;\n//    }\n//\n//    public void setShadowDrawable(Drawable shadowDrawable) {\n//        mShadowDrawable = shadowDrawable;\n//    }\n//\n//    public void setup() {\n//        Log.e(\"Sticky\", \"setup\");\n//        stickyViews = new ArrayList<>();\n//    }\n//\n//    private int getLeftForViewRelativeOnlyChild(View v) {\n//        Log.e(\"Sticky\", \"getLeftForViewRelativeOnlyChild\");\n//        int left = v.getLeft();\n//        while (v.getParent() != null && v.getParent() != getChildAt(0)) {\n//            v = (View)v.getParent();\n//            left += v.getLeft();\n//        }\n//        return left;\n//    }\n//\n//    private int getTopForViewRelativeOnlyChild(View v) {\n//        Log.e(\"Sticky\", \"getTopForViewRelativeOnlyChild\");\n//        int top = v.getTop();\n//        while (v.getParent() != null && v.getParent() != getChildAt(0)) {\n//            v = (View)v.getParent();\n//            top += v.getTop();\n//        }\n//        return top;\n//    }\n//\n//    private int getRightForViewRelativeOnlyChild(View v) {\n//        Log.e(\"Sticky\", \"getRightForViewRelativeOnlyChild\");\n//        int right = v.getRight();\n//        while (v.getParent() != null && v.getParent() != getChildAt(0)) {\n//            v = (View)v.getParent();\n//            right += v.getRight();\n//        }\n//        return right;\n//    }\n//\n//    private int getBottomForViewRelativeOnlyChild(View v) {\n//        Log.e(\"Sticky\", \"getBottomForViewRelativeOnlyChild\");\n//        int bottom = v.getBottom();\n//        while (v.getParent() != null && v.getParent() != getChildAt(0)) {\n//            v = (View)v.getParent();\n//            bottom += v.getBottom();\n//        }\n//        return bottom;\n//    }\n//\n//    @Override\n//    protected void onLayout(boolean changed, int l, int t, int r, int b) {\n//        super.onLayout(changed, l, t, r, b);\n//        Log.e(\"Sticky\", \"onLayout\");\n//        if (!clipToPaddingHasBeenSet) {\n//            clippingToPadding = true;\n//        }\n//        notifyHierarchyChanged();\n//    }\n//\n//    @Override\n//    public void setClipToPadding(boolean clipToPadding) {\n//        super.setClipToPadding(clipToPadding);\n//        Log.e(\"Sticky\", \"setClipToPadding\");\n//        clippingToPadding = clipToPadding;\n//        clipToPaddingHasBeenSet = true;\n//    }\n//\n//    @Override\n//    public void addView(View child) {\n//        super.addView(child);\n//        Log.e(\"Sticky\", \"addView   1  \");\n//        findStickyViews(child);\n//    }\n//\n//    @Override\n//    public void addView(View child, int index) {\n//        super.addView(child, index);\n//        Log.e(\"Sticky\", \"addView   2  \");\n//        findStickyViews(child);\n//    }\n//\n//    @Override\n//    public void addView(View child, int index, ViewGroup.LayoutParams params) {\n//        super.addView(child, index, params);\n//        Log.e(\"Sticky\", \"addView   3  \");\n//        findStickyViews(child);\n//    }\n//\n//    @Override\n//    public void addView(View child, int width, int height) {\n//        super.addView(child, width, height);\n//        Log.e(\"Sticky\", \"addView   4 \");\n//        findStickyViews(child);\n//    }\n//\n//    @Override\n//    public void addView(View child, ViewGroup.LayoutParams params) {\n//        super.addView(child, params);\n//        Log.e(\"Sticky\", \"addView   5  \");\n//        findStickyViews(child);\n//    }\n//\n//    @Override\n//    protected void dispatchDraw(Canvas canvas) {\n//        super.dispatchDraw(canvas);\n//        Log.e(\"Sticky\", \"dispatchDraw\");\n//        if (currentlyStickingView != null) {\n//            canvas.save();\n//            canvas.translate(getPaddingLeft() + stickyViewLeftOffset,\n//                getScrollY() + stickyViewTopOffset + (clippingToPadding ? getPaddingTop() : 0));\n//            canvas.clipRect(0, (clippingToPadding ? -stickyViewTopOffset : 0), getWidth() - stickyViewLeftOffset,\n//                currentlyStickingView.getHeight() + mShadowHeight + 1);\n//            if (mShadowDrawable != null) {\n//                int left = 0;\n//                int top = currentlyStickingView.getHeight();\n//                int right = currentlyStickingView.getWidth();\n//                int bottom = currentlyStickingView.getHeight() + mShadowHeight;\n//                mShadowDrawable.setBounds(left, top, right, bottom);\n//                mShadowDrawable.draw(canvas);\n//            }\n//            canvas.clipRect(0, (clippingToPadding ? -stickyViewTopOffset : 0), getWidth(),\n//                currentlyStickingView.getHeight());\n//            if (getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARENCY)) {\n//                showView(currentlyStickingView);\n//                currentlyStickingView.draw(canvas);\n//                hideView(currentlyStickingView);\n//            } else {\n//                currentlyStickingView.draw(canvas);\n//            }\n//            canvas.restore();\n//        }\n//    }\n//\n//    @Override\n//    public boolean dispatchTouchEvent(MotionEvent ev) {\n//        Log.e(\"Sticky\", \"dispatchTouchEvent\");\n//        if (ev.getAction() == MotionEvent.ACTION_DOWN) {\n//            redirectTouchesToStickyView = true;\n//        }\n//        if (redirectTouchesToStickyView) {\n//            redirectTouchesToStickyView = currentlyStickingView != null;\n//            if (redirectTouchesToStickyView) {\n//                redirectTouchesToStickyView = ev.getY() <= (currentlyStickingView.getHeight() + stickyViewTopOffset) &&\n//                    ev.getX() >= getLeftForViewRelativeOnlyChild(currentlyStickingView) &&\n//                    ev.getX() <= getRightForViewRelativeOnlyChild(currentlyStickingView);\n//            }\n//        } else if (currentlyStickingView == null) {\n//            redirectTouchesToStickyView = false;\n//        }\n//        if (redirectTouchesToStickyView) {\n//            ev.offsetLocation(0,\n//                -1 * ((getScrollY() + stickyViewTopOffset) - getTopForViewRelativeOnlyChild(currentlyStickingView)));\n//        }\n//        return super.dispatchTouchEvent(ev);\n//    }\n//\n//    @Override\n//    public boolean onTouchEvent(MotionEvent ev) {\n//        Log.e(\"Sticky\", \"onTouchEvent\");\n//        if (redirectTouchesToStickyView) {\n//            ev.offsetLocation(0,\n//                ((getScrollY() + stickyViewTopOffset) - getTopForViewRelativeOnlyChild(currentlyStickingView)));\n//        }\n//        if (ev.getAction() == MotionEvent.ACTION_DOWN) {\n//            hasNotDoneActionDown = false;\n//        }\n//        if (hasNotDoneActionDown) {\n//            MotionEvent down = MotionEvent.obtain(ev);\n//            down.setAction(MotionEvent.ACTION_DOWN);\n//            super.onTouchEvent(down);\n//            hasNotDoneActionDown = false;\n//        }\n//        if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {\n//            hasNotDoneActionDown = true;\n//        }\n//        return super.onTouchEvent(ev);\n//    }\n//\n//    @Override\n//    protected void onScrollChanged(int l, int t, int oldl, int oldt) {\n//        super.onScrollChanged(l, t, oldl, oldt);\n//        Log.e(\"Sticky\", \"处理滚动：\" + l + \" , \" + t + \" , \" + oldl + \" , \" + oldt);\n//        doTheStickyThing();\n//    }\n//\n//    private void doTheStickyThing() {\n//        View viewThatShouldStick = null;\n//        // 接近视图\n//        View approachingView = null;\n//        for (View v : stickyViews) {\n//            int viewTop = getTopForViewRelativeOnlyChild(v) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop());\n//            if (viewTop <= 0) {\n//                if (viewThatShouldStick == null || viewTop >\n//                    (getTopForViewRelativeOnlyChild(viewThatShouldStick) - getScrollY() +\n//                        (clippingToPadding ? 0 : getPaddingTop()))) {\n//                    viewThatShouldStick = v;\n//                }\n//            } else {\n//                if (approachingView == null || viewTop <\n//                    (getTopForViewRelativeOnlyChild(approachingView) - getScrollY() +\n//                        (clippingToPadding ? 0 : getPaddingTop()))) {\n//                    approachingView = v;\n//                }\n//            }\n//        }\n//        if (viewThatShouldStick != null) {\n//            stickyViewTopOffset = approachingView == null\n//                ? 0\n//                : Math.min(0, getTopForViewRelativeOnlyChild(approachingView) - getScrollY() +\n//                    (clippingToPadding ? 0 : getPaddingTop()) - viewThatShouldStick.getHeight());\n//            if (viewThatShouldStick != currentlyStickingView) {\n//                if (currentlyStickingView != null) {\n//                    if (mOnViewStickyListeners != null) {\n//                        for (OnViewStickyListener onViewStickyListener : mOnViewStickyListeners)\n//                            onViewStickyListener.onUnSticky(currentlyStickingView);\n//                    }\n//                    stopStickingCurrentlyStickingView();\n//                }\n//                // only compute the left offset when we start sticking.\n//                stickyViewLeftOffset = getLeftForViewRelativeOnlyChild(viewThatShouldStick);\n//                startStickingView(viewThatShouldStick);\n//                if (mOnViewStickyListeners != null) {\n//                    for (OnViewStickyListener onViewStickyListener : mOnViewStickyListeners)\n//                        onViewStickyListener.onSticky(currentlyStickingView);\n//                }\n//            }\n//        } else if (currentlyStickingView != null) {\n//            if (mOnViewStickyListeners != null) {\n//                for (OnViewStickyListener onViewStickyListener : mOnViewStickyListeners)\n//                    onViewStickyListener.onUnSticky(currentlyStickingView);\n//            }\n//            stopStickingCurrentlyStickingView();\n//        }\n//    }\n//\n//    private void startStickingView(View viewThatShouldStick) {\n//        Log.e(\"Sticky\", \"startStickingView\");\n//        currentlyStickingView = viewThatShouldStick;\n//        if (currentlyStickingView != null) {\n//            if (getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARENCY)) {\n//                hideView(currentlyStickingView);\n//            }\n//            if (getStringTagForView(currentlyStickingView).contains(FLAG_NONCONSTANT)) {\n//                post(invalidateRunnable);\n//            }\n//        }\n//    }\n//\n//    private void stopStickingCurrentlyStickingView() {\n//        Log.e(\"Sticky\", \"stopStickingCurrentlyStickingView\");\n//        if (getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARENCY)) {\n//            showView(currentlyStickingView);\n//        }\n//        currentlyStickingView = null;\n//        removeCallbacks(invalidateRunnable);\n//    }\n//\n//    @Override\n//    protected void onDetachedFromWindow() {\n//        Log.e(\"Sticky\", \"onDetachedFromWindow\");\n//        removeCallbacks(invalidateRunnable);\n//        super.onDetachedFromWindow();\n//    }\n//\n//    /**\n//     * Notify that the sticky attribute has been added or removed from one or more views in the View hierarchy\n//     */\n//    public void notifyStickyAttributeChanged() {\n//        notifyHierarchyChanged();\n//    }\n//\n//    // 通知等级制度改变\n//    private void notifyHierarchyChanged() {\n//        Log.e(\"Sticky\", \"notifyHierarchyChanged\");\n//        if (currentlyStickingView != null) {\n//            stopStickingCurrentlyStickingView();\n//        }\n//        stickyViews.clear();\n//        findStickyViews(getChildAt(0));\n//        doTheStickyThing();\n//        invalidate();\n//    }\n//\n//    private void findStickyViews(View v) {\n//        Log.e(\"Sticky\", \"findStickyViews\");\n//        if (!detainStickyView(v) && (v instanceof ViewGroup)) {\n//            ViewGroup vg = (ViewGroup)v;\n//            for (int i = 0; i < vg.getChildCount(); i++)\n//                findStickyViews(vg.getChildAt(i));\n//        }\n//    }\n//\n//    // 扣留粘性视图\n//    private boolean detainStickyView(View view) {\n//        Log.e(\"Sticky\", \"detainStickyView\");\n//        String tag = getStringTagForView(view);\n//        if (tag.contains(STICKY_TAG)) {\n//            stickyViews.add(view);\n//            return true;\n//        }\n//        return false;\n//    }\n//\n//    private String getStringTagForView(View v) {\n//        Log.e(\"Sticky\", \"getStringTagForView\");\n//        Object tagObject = v.getTag();\n//        return String.valueOf(tagObject);\n//    }\n//\n//    private void hideView(View v) {\n//        v.setAlpha(0);\n//    }\n//\n//    private void showView(View v) {\n//        v.setAlpha(1);\n//    }\n//\n//}"
  },
  {
    "path": "support/src/main/res/layout/support_recycler_view_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright 2017 Yan Zhenjie\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distribucheckedd on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n-->\n<com.yanzhenjie.recyclerview.SwipeMenuLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    app:contentViewId=\"@+id/swipe_content\"\n    app:leftViewId=\"@+id/swipe_left\"\n    app:rightViewId=\"@+id/swipe_right\">\n\n    <com.yanzhenjie.recyclerview.SwipeMenuView\n        android:id=\"@+id/swipe_left\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"/>\n\n    <FrameLayout\n        android:id=\"@+id/swipe_content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"/>\n\n    <com.yanzhenjie.recyclerview.SwipeMenuView\n        android:id=\"@+id/swipe_right\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"/>\n\n</com.yanzhenjie.recyclerview.SwipeMenuLayout>"
  },
  {
    "path": "support/src/main/res/layout/support_recycler_view_item2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright 2017 Yan Zhenjie\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distribucheckedd on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n-->\n<com.yanzhenjie.recyclerview.SwipeDragLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    app:contentViewId=\"@+id/swipe_content\"\n    app:leftViewId=\"@+id/swipe_left\"\n    app:rightViewId=\"@+id/swipe_right\">\n\n    <com.yanzhenjie.recyclerview.SwipeMenuView\n        android:id=\"@+id/swipe_left\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"/>\n\n    <com.yanzhenjie.recyclerview.SwipeMenuView\n        android:id=\"@+id/swipe_right\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"/>\n\n    <FrameLayout\n        android:id=\"@+id/swipe_content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"/>\n\n\n</com.yanzhenjie.recyclerview.SwipeDragLayout>"
  },
  {
    "path": "support/src/main/res/layout/support_recycler_view_load_more.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright 2017 Yan Zhenjie\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distribucheckedd on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n-->\n<merge\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <ProgressBar\n        android:id=\"@+id/progress_bar\"\n        style=\"@android:style/Widget.Holo.ProgressBar\"\n        android:layout_width=\"32dp\"\n        android:layout_height=\"32dp\"\n        android:visibility=\"gone\"\n        tools:visibility=\"invisible\"/>\n\n    <TextView\n        android:id=\"@+id/tv_load_more_message\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center_vertical\"\n        android:textColor=\"@color/support_recycler_color_text_gray\"\n        android:visibility=\"gone\"\n        tools:text=\"Loading...\"\n        tools:visibility=\"invisible\"/>\n\n</merge>"
  },
  {
    "path": "support/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright 2017 Yan Zhenjie\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distribucheckedd on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n-->\n<resources>\n\n    <declare-styleable name=\"SwipeMenuLayout\">\n        <attr name=\"leftViewId\" format=\"reference|integer\"/>\n        <attr name=\"rightViewId\" format=\"reference|integer\"/>\n        <attr name=\"contentViewId\" format=\"reference|integer\"/>\n    </declare-styleable>\n\n</resources>"
  },
  {
    "path": "support/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright 2017 Yan Zhenjie\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distribucheckedd on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n-->\n<resources>\n\n    <color name=\"support_recycler_color_text_gray\">#FF777777</color>\n\n    <color name=\"support_recycler_color_loading_color1\">#55777777</color>\n    <color name=\"support_recycler_color_loading_color2\">#B1777777</color>\n    <color name=\"support_recycler_color_loading_color3\">#FF777777</color>\n\n</resources>"
  },
  {
    "path": "support/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright 2017 Yan Zhenjie\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distribucheckedd on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n-->\n<resources>\n\n    <string name=\"support_recycler_data_empty\">There is no data</string>\n    <string name=\"support_recycler_more_not\">No more data</string>\n    <string name=\"support_recycler_click_load_more\">Click to load more</string>\n    <string name=\"support_recycler_load_error\">Error, please try again</string>\n    <string name=\"support_recycler_load_more_message\">Loading, please wait later</string>\n\n</resources>"
  },
  {
    "path": "support/src/main/res/values-zh/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright 2017 Yan Zhenjie\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distribucheckedd on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n-->\n<resources>\n\n    <string name=\"support_recycler_data_empty\">暂时没有数据</string>\n    <string name=\"support_recycler_more_not\">没有更多数据啦</string>\n    <string name=\"support_recycler_click_load_more\">点击加载更多</string>\n    <string name=\"support_recycler_load_error\">加载出错啦，请稍后重试</string>\n    <string name=\"support_recycler_load_more_message\">正在加载更多数据，请稍后</string>\n\n</resources>"
  },
  {
    "path": "support/src/main/res/values-zh-rHK/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright 2017 Yan Zhenjie\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distribucheckedd on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n-->\n<resources>\n\n    <string name=\"support_recycler_data_empty\">暫時沒有數據</string>\n    <string name=\"support_recycler_more_not\">沒有更多數據啦</string>\n    <string name=\"support_recycler_click_load_more\">點擊加載更多</string>\n    <string name=\"support_recycler_load_error\">加載出錯啦，請稍後重試</string>\n    <string name=\"support_recycler_load_more_message\">正在加載更多數據，請稍後</string>\n\n</resources>"
  },
  {
    "path": "support/src/main/res/values-zh-rTW/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    Copyright 2017 Yan Zhenjie\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distribucheckedd on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n-->\n<resources>\n\n    <string name=\"support_recycler_data_empty\">暫時沒有數據</string>\n    <string name=\"support_recycler_more_not\">沒有更多數據啦</string>\n    <string name=\"support_recycler_click_load_more\">點擊加載更多</string>\n    <string name=\"support_recycler_load_error\">加載出錯啦，請稍後重試</string>\n    <string name=\"support_recycler_load_more_message\">正在加載更多數據，請稍後</string>\n\n</resources>"
  },
  {
    "path": "swipelayout/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "swipelayout/build.gradle",
    "content": "apply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion 29\n\n    defaultConfig {\n        minSdkVersion 22\n        targetSdkVersion 29\n        versionCode 1\n        versionName \"1.0\"\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    testImplementation 'junit:junit:4.13'\n    implementation 'androidx.legacy:legacy-support-v4:1.0.0'\n}\n"
  },
  {
    "path": "swipelayout/proguard-rules.pro",
    "content": "# Add project specific ProGuard replaceUrl here.\n# By default, the flags in this file are appended to flags specified\n# in /Users/vienan/Library/Android/sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class userName to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n"
  },
  {
    "path": "swipelayout/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          package=\"com.ditclear.swipelayout\">\n\n    <application\n        android:allowBackup=\"true\"\n        android:label=\"@string/app_name\"\n        android:supportsRtl=\"true\">\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "swipelayout/src/main/java/com/ditclear/swipelayout/SwipeDragLayout.java",
    "content": "package com.ditclear.swipelayout;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Point;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.view.Gravity;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewConfiguration;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\n\nimport androidx.customview.widget.ViewDragHelper;\n\n/**\n * @author ditclear on 16/7/12. 可滑动的layout extends FrameLayout\n * https://github.com/ditclear/TimeLine/blob/master/swipelayout/src/main/java/com/ditclear/swipelayout/SwipeDragLayout.java\n * 实现主页的左右滑动已读未读\n */\npublic class SwipeDragLayout extends FrameLayout {\n    private SwipeDragLayout mCacheView;\n    private View contentView;\n    private View leftMenuView;\n    private View rightMenuView;\n    private ViewDragHelper mDragHelper;\n    private Point originPos = new Point();\n    private boolean isOpen, ios, clickToClose;\n    private float offsetRatio;\n    private float needOffset; // 默认值为 0.45\n    private SwipeListener mListener;\n\n    public SwipeDragLayout(Context context) {\n        this(context, null);\n    }\n\n    public SwipeDragLayout(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public SwipeDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n\n        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SwipeDragLayout);\n        needOffset = array.getFloat(R.styleable.SwipeDragLayout_need_offset, 0.6f);\n        //是否有回弹效果\n        ios = array.getBoolean(R.styleable.SwipeDragLayout_ios, false);\n        clickToClose = array.getBoolean(R.styleable.SwipeDragLayout_click_to_close, false);\n        init();\n        array.recycle();\n    }\n\n//    public static SwipeDragLayout getmCacheView() {\n//        return mCacheView;\n//    }\n\n    // 初始化dragHelper，对拖动的view进行操作\n    private void init() {\n        // ViewDragHelper中拦截和处理事件时，需要会回调CallBack中的很多方法来决定一些事，比如：哪些子View可以移动、对个移动的View的边界的控制等等。\n        mDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {\n\n            // 返回ture则表示可以捕获该view，你可以根据传入的第一个view参数决定哪些可以捕获\n            @Override\n            public boolean tryCaptureView(View child, int pointerId) {\n                return child == contentView;\n            }\n\n\n            /**\n             * 用来修正或者指定子View在水平方向上的移动\n             * clampViewPositionHorizontal 可以在该方法中对child移动的边界进行控制，left 分别为即将移动到的位置。\n             * 比如横向的情况下，我希望只在ViewGroup的内部移动，即：最小>=paddingleft，最大<=ViewGroup.getWidth()-paddingright-child.getWidth。\n             * @param child 被拖动的view\n             * @param left  是ViewDragHelper帮你计算好的View最新的left的值，left=view.getLeft()+dx\n             * @param dx   本次水平移动的距离\n             * @return  返回的值表示我们真正想让View的left变成的值\n             */\n            @Override\n            public int clampViewPositionHorizontal(View child, int left, int dx) {\n//                //滑动距离,如果启动效果，则可滑动3/2倍菜单宽度的距离\n//                // 左边限 = -156\n//                final int leftBound = getPaddingLeft() - (ios ? rightMenuView.getWidth() * 3 / 2 : rightMenuView.getWidth());\n//                // 右边限 = 0\n//                final int rightBound = getWidth() - child.getWidth();\n//                final int newLeft = Math.min(Math.max(left, leftBound), rightBound);\n////                mListener.log(\"clampViewPositionHorizontal：leftBound=\" + leftBound + \"  rightBound=\" + rightBound +\"  newLeft=\"+ newLeft + \"  \" + left );\n////                return newLeft;\n                return left;\n            }\n\n\n//            // 要返回一个大于0的数，才会在在水平方向上对触摸到的View进行拖动。\n//            // 方法的返回值应当是该childView横向或者纵向的移动的范围，当前如果只需要一个方向移动，可以只复写一个。\n//            // 方法名为获取水平方向拖拽的范围，然而目前并没有用，该方法的返回值用来作为判断滑动方向的条件之一， 如果你想水平移动，那么该方法的返回值最好大于0\n            @Override\n            public int getViewHorizontalDragRange(View child) {\n//                mListener.log(\"getViewHorizontalDragRange\" );\n//                return contentView == child ? rightMenuView.getWidth() : 0;\n                return 1;\n            }\n            /**\n             * 当View移动的时候调用\n             * @param changedView   当前移动的VIew\n             * @param left  当前View移动之后最新的left(应该是相对初始点的偏移量)\n             * @param top   当前View移动之后最新的top(应该是相对初始点的偏移量)\n             * @param dx    水平移动的距离\n             * @param dy    垂直移动的距离\n             */\n            @Override\n            public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {\n                Log.e(\"滑动\", \"滑动：\" + left + \"   \" + top  + \"   \" + dx  + \"   \" + dy  );\n                final int childWidth = rightMenuView.getWidth();\n                offsetRatio = -(float) (left - getPaddingLeft()) / childWidth;\n                //offsetRatio can callback here\n                if (mListener!=null){\n                    mListener.onUpdate(SwipeDragLayout.this, offsetRatio,left);\n                }\n//                mListener.log(\"onViewPositionChanged：\" + offsetRatio + \"  left:\" + left + \"   top\" + top + \"  dx\" + dx + \"   dy\" + dy );\n                // 兼容老版本\n                invalidate();\n            }\n\n            //手指释放的时候回调\n            @Override\n            public void onViewReleased(View releasedChild, float xvel, float yvel) {\n                Log.e(\"滑动\", \"释放：\" + xvel + \"   \" + yvel );\n//                mListener.log(\"-------------------------------------------------------------手指释放\" );\n                // Note: needOffset 最小偏移量 应该由左/右菜单View 的宽度来定。（）\n                if (releasedChild == contentView) {\n                    if (isOpen()) {\n                        if (offsetRatio != 1 && offsetRatio > (1 - needOffset)) {\n                            openRight();\n//                            mListener.log(\"A：\" + offsetRatio + \"  \" + needOffset);\n                        } else if (offsetRatio == 1) {\n                            if (clickToClose) {\n                                close();\n                            }\n//                            mListener.log(\"B：\" + offsetRatio + \"  \" + clickToClose);\n                        } else {\n                            if( Math.abs(offsetRatio) < (1 - needOffset) ){\n                                close();\n                            }else {\n                                openLeft();\n                            }\n//                            close();\n//                            mListener.log(\"C：\" + offsetRatio + \"  \" + clickToClose);\n                        }\n                    } else {\n                        if (offsetRatio != 0 && offsetRatio < needOffset) {\n                            if( Math.abs(offsetRatio) < needOffset ){\n                                close();\n                            }else {\n                                openLeft();\n                            }\n//                            mListener.log(\"D：\" + offsetRatio + \"  \" + needOffset);\n                        } else if (offsetRatio == 0) {\n                            getParent().requestDisallowInterceptTouchEvent(false);\n//                            mListener.log(\"E：\" + offsetRatio + \"  \" + needOffset);\n                        } else {\n                            openRight();\n//                            mListener.log(\"F：\" + offsetRatio + \"  \" + isOpen + \"  \" + needOffset);\n                        }\n                    }\n                    invalidate();\n                }\n            }\n        });\n    }\n\n    public void setClickToClose(boolean clickToClose) {\n        this.clickToClose = clickToClose;\n    }\n\n    public void setIos(boolean ios) {\n        this.ios = ios;\n    }\n\n    public boolean isOpen() {\n        return isOpen;\n    }\n\n    public void openRight() {\n        mCacheView = SwipeDragLayout.this;\n        mDragHelper.settleCapturedViewAt(originPos.x - rightMenuView.getWidth(), originPos.y);\n        isOpen = true;\n//        mListener.log(\"打开右侧\"  + isOpen );\n//        Log.d(\"Released and isOpen\", \"\" + isOpen);\n        if (mListener != null) {\n            mListener.onOpened(SwipeDragLayout.this);\n            closeRight();\n        }\n    }\n\n    public void openLeft() {\n        mCacheView = SwipeDragLayout.this;\n        mDragHelper.settleCapturedViewAt(originPos.x + leftMenuView.getWidth(), originPos.y);\n        isOpen = true;\n//        mListener.log(\"打开左侧\" + isOpen );\n        if (mListener != null) {\n            mListener.onOpened(SwipeDragLayout.this);\n            closeLeft();\n        }\n    }\n\n//    public void smoothOpenRight(boolean smooth) {\n//        mCacheView = SwipeDragLayout.this;\n//        if (smooth) {\n//            mDragHelper.smoothSlideViewTo(contentView, originPos.x - rightMenuView.getWidth(), originPos.y);\n//        } else {\n//            contentView.layout(originPos.x - rightMenuView.getWidth(), originPos.y, rightMenuView.getLeft(), rightMenuView.getBottom());\n//        }\n//    }\n//    public void smoothOpenLeft(boolean smooth) {\n//        mCacheView = SwipeDragLayout.this;\n//        if (smooth) {\n//            mDragHelper.smoothSlideViewTo(contentView, originPos.x + leftMenuView.getWidth(), originPos.y);\n//        } else {\n//            contentView.layout(originPos.x + leftMenuView.getWidth(), originPos.y, leftMenuView.getLeft(), leftMenuView.getBottom());\n//        }\n//    }\n\n    private void smoothClose(boolean smooth) {\n        if (smooth) {\n            mDragHelper.smoothSlideViewTo(contentView, getPaddingLeft(), getPaddingTop());\n            postInvalidate();\n        } else {\n            contentView.layout(originPos.x, originPos.y, rightMenuView.getRight(), rightMenuView.getBottom());\n        }\n        isOpen = false;\n        mCacheView = null;\n\n    }\n\n\n\n    public void close() {\n        mDragHelper.settleCapturedViewAt(originPos.x, originPos.y);\n        isOpen = false;\n        mCacheView = null;\n        mListener.onClosed(SwipeDragLayout.this);\n    }\n\n    public void closeLeft() {\n        if (contentView == null){\n            return;\n        }\n        try {\n            mDragHelper.settleCapturedViewAt(originPos.x, originPos.y);\n        }catch (Exception e){\n        }\n        isOpen = false;\n        mCacheView = null;\n        mListener.onCloseLeft(SwipeDragLayout.this);\n    }\n    public void closeRight() {\n        if (contentView == null){\n            return;\n        }\n        try {\n            mDragHelper.settleCapturedViewAt(originPos.x, originPos.y);\n        }catch (Exception e){\n        }\n        isOpen = false;\n        mCacheView = null;\n        mListener.onCloseRight(SwipeDragLayout.this);\n//        if (mListener != null) {\n//            mListener.onCloseRight(SwipeDragLayout.this);\n//        }\n    }\n\n    private int mLastX = 0;\n    private int mLastY = 0;\n    @Override\n    public boolean dispatchTouchEvent(MotionEvent event) {\n        int eventX = (int) event.getX();\n        int eventY = (int) event.getY();\n        switch (event.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                mLastX = (int) event.getX();\n                mLastY = (int) event.getY();\n                break;\n            case MotionEvent.ACTION_MOVE:\n                int offsetX = eventX - mLastX;\n                int offsetY = eventY - mLastY;\n                if ( Math.abs(offsetX) - Math.abs(offsetY) < ViewConfiguration.getTouchSlop()) {\n                    break;\n                }\n                getParent().requestDisallowInterceptTouchEvent(true);\n                break;\n        }\n        mLastX = eventX;\n        mLastY = eventY;\n        return super.dispatchTouchEvent(event);\n    }\n\n\n\n\n    @Override\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n//        mListener.log(\"拦截触摸事件\" + ev.getAction() );\n        switch (ev.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                if (mCacheView != null) {\n                    if (mCacheView != this) {\n                        mCacheView.smoothClose(true);\n                    }\n                    getParent().requestDisallowInterceptTouchEvent(true);\n                }\n                break;\n        }\n        // onInterceptTouchEvent中通过使用mDragger.shouldInterceptTouchEvent(event)来决定我们是否应该拦截当前的事件。\n        return mDragHelper.shouldInterceptTouchEvent(ev);\n    }\n\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        mDragHelper.processTouchEvent(event); // onTouchEvent中通过mDragger.processTouchEvent(event)处理事件。\n        return true;\n    }\n\n\n    @Override\n    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n        super.onLayout(changed, left, top, right, bottom);\n        originPos.x = contentView.getLeft();\n        originPos.y = contentView.getTop();\n    }\n\n    @Override\n    public void computeScroll() {\n        if (mDragHelper.continueSettling(true)) {\n            invalidate();\n        }\n    }\n\n    // 当View中所有的子控件均被映射成xml后触发.当加载完成xml后，就会执行这个方法。\n    @Override\n    protected void onFinishInflate() {\n        super.onFinishInflate();\n        leftMenuView = getChildAt(0);\n        rightMenuView = getChildAt(1);\n        contentView = getChildAt(2);\n\n        FrameLayout.LayoutParams params2 = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);\n        params2.gravity = Gravity.CENTER_VERTICAL|Gravity.LEFT;\n        leftMenuView.setLayoutParams(params2);\n\n        FrameLayout.LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);\n        params.gravity = Gravity.RIGHT|Gravity.CENTER_VERTICAL;\n        rightMenuView.setLayoutParams(params);\n\n\n        //重写OnClickListener会导致关闭失效\n        if (contentView != null){\n            contentView.setOnClickListener(new OnClickListener() {\n                @Override\n                public void onClick(View v) {\n                    if (clickToClose&&isOpen()){\n                        smoothClose(true);\n                        return;\n                    }\n                    if (mListener!=null){\n                        mListener.onClick(SwipeDragLayout.this);\n                    }\n\n                }\n            });\n        }\n    }\n\n    // onDetachedFromWindow方法是在Activity destroy的时候被调用的，也就是act对应的window被删除的时候，且每个view只会被调用一次，父view的调用在后，也不论view的visibility状态都会被调用，适合做最后的清理操作；\n    @Override\n    protected void onDetachedFromWindow() {\n        if (mCacheView == this) {\n            mCacheView.smoothClose(false);\n            mCacheView = null;\n        }\n        super.onDetachedFromWindow();\n    }\n\n    public static final int DIRECTION_LEFT = 1;\n    public static final int DIRECTION_RIGHT = -1;\n\n    public void addListener(SwipeListener listener) {\n        mListener = listener;\n    }\n\n    //滑动监听\n    public interface SwipeListener {\n        /**\n         * 拖动中，可根据offset 进行其他动画\n         * @param view\n         * @param offset 偏移量\n         */\n        void onUpdate(View  view, float offsetRatio,int offset);\n\n        /**\n         * 展开完成\n         * @param view\n         */\n        void onOpened(View  view);\n\n        /**\n         * 关闭完成\n         * @param view\n         */\n        void onClosed(View  view);\n        void onCloseLeft(View  view);\n        void onCloseRight(View  view);\n\n        /**\n         * 点击内容layout {@link #onFinishInflate()}\n         * @param view\n         */\n        void onClick(View  view);\n\n        void log(String temp);\n    }\n\n}\n"
  },
  {
    "path": "swipelayout/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <declare-styleable name=\"SwipeDragLayout\">\n        <attr name=\"need_offset\" format=\"float\"/>\n        <attr name=\"ios\" format=\"boolean\"/>\n        <attr name=\"click_to_close\" format=\"boolean\"/>\n\n        <!--<auserNamename=\"swipe_enable\" format=\"boolean\"/>-->\n        <!--<auserNamename=\"swipe_direction\" format=\"enum\">-->\n            <!--<euserNamename=\"left\" value=\"1\"/>-->\n            <!--<euserNamename=\"right\" value=\"2\"/>-->\n        <!--</attr>-->\n    </declare-styleable>\n</resources>"
  },
  {
    "path": "swipelayout/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">RSSLibrary</string>\n</resources>\n"
  }
]