diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 14e20d468..d7067dbd3 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -138,6 +138,12 @@
android:theme="@style/AppTheme.NoActionBar"
android:label="@string/u_config"/>
+
+
diff --git a/app/src/main/java/com/hippo/ehviewer/EhApplication.java b/app/src/main/java/com/hippo/ehviewer/EhApplication.java
index 4eb273b9f..c783188f5 100644
--- a/app/src/main/java/com/hippo/ehviewer/EhApplication.java
+++ b/app/src/main/java/com/hippo/ehviewer/EhApplication.java
@@ -31,6 +31,7 @@ import com.hippo.beerbelly.SimpleDiskCache;
import com.hippo.conaco.Conaco;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhCookieStore;
+import com.hippo.ehviewer.client.EhDns;
import com.hippo.ehviewer.client.EhEngine;
import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.ehviewer.download.DownloadManager;
@@ -81,6 +82,7 @@ public class EhApplication extends SceneApplication implements Thread.UncaughtEx
private LruCache mGalleryDetailCache;
private SimpleDiskCache mSpiderInfoCache;
private DownloadManager mDownloadManager;
+ private Hosts mHosts;
private final List mActivityList = new ArrayList<>();
@@ -238,6 +240,7 @@ public class EhApplication extends SceneApplication implements Thread.UncaughtEx
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.cookieJar(getEhCookieStore(application))
+ .dns(new EhDns(application))
.build();
}
return application.mOkHttpClient;
@@ -303,6 +306,15 @@ public class EhApplication extends SceneApplication implements Thread.UncaughtEx
return application.mDownloadManager;
}
+ @NonNull
+ public static Hosts getHosts(@NonNull Context context) {
+ EhApplication application = ((EhApplication) context.getApplicationContext());
+ if (application.mHosts == null) {
+ application.mHosts = new Hosts(application, "hosts.db");
+ }
+ return application.mHosts;
+ }
+
private boolean handleException(Throwable ex) {
if (ex == null) {
return false;
diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhDns.java b/app/src/main/java/com/hippo/ehviewer/client/EhDns.java
new file mode 100644
index 000000000..2472d95d4
--- /dev/null
+++ b/app/src/main/java/com/hippo/ehviewer/client/EhDns.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018 Hippo Seven
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.client;
+
+/*
+ * Created by Hippo on 2018/3/23.
+ */
+
+import android.content.Context;
+import com.hippo.ehviewer.EhApplication;
+import com.hippo.ehviewer.Hosts;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import okhttp3.Dns;
+
+public class EhDns implements Dns {
+
+ private final Hosts hosts;
+
+ public EhDns(Context context) {
+ hosts = EhApplication.getHosts(context);
+ }
+
+ @Override
+ public List lookup(String hostname) throws UnknownHostException {
+ if (hostname == null) throw new UnknownHostException("hostname == null");
+
+ InetAddress inetAddress = hosts.get(hostname);
+ if (inetAddress != null) {
+ return Collections.singletonList(inetAddress);
+ }
+
+ try {
+ return Arrays.asList(InetAddress.getAllByName(hostname));
+ } catch (NullPointerException e) {
+ UnknownHostException unknownHostException =
+ new UnknownHostException("Broken system behaviour for dns lookup of " + hostname);
+ unknownHostException.initCause(e);
+ throw unknownHostException;
+ }
+ }
+}
diff --git a/app/src/main/java/com/hippo/ehviewer/ui/HostsActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/HostsActivity.java
new file mode 100644
index 000000000..4c382bec1
--- /dev/null
+++ b/app/src/main/java/com/hippo/ehviewer/ui/HostsActivity.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2018 Hippo Seven
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.ui;
+
+/*
+ * Created by Hippo on 2018/3/23.
+ */
+
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.design.widget.FloatingActionButton;
+import android.support.design.widget.TextInputLayout;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import com.hippo.easyrecyclerview.EasyRecyclerView;
+import com.hippo.easyrecyclerview.LinearDividerItemDecoration;
+import com.hippo.ehviewer.EhApplication;
+import com.hippo.ehviewer.Hosts;
+import com.hippo.ehviewer.R;
+import com.hippo.ripple.Ripple;
+import com.hippo.yorozuya.LayoutUtils;
+import java.util.List;
+import java.util.Locale;
+
+public class HostsActivity extends ToolbarActivity
+ implements EasyRecyclerView.OnItemClickListener, View.OnClickListener {
+
+ private static final String DIALOG_TAG_ADD_HOST = AddHostDialogFragment.class.getName();
+ private static final String DIALOG_TAG_EDIT_HOST = EditHostDialogFragment.class.getName();
+
+ private static final String KEY_HOST = "com.hippo.ehviewer.ui.HostsActivity.HOST";
+ private static final String KEY_IP = "com.hippo.ehviewer.ui.HostsActivity.IP";
+
+ private Hosts hosts;
+ private List> data;
+
+ private EasyRecyclerView recyclerView;
+ private View tip;
+ private HostsAdapter adapter;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ hosts = EhApplication.getHosts(this);
+ data = hosts.getAll();
+
+ setContentView(R.layout.activity_hosts);
+ setNavigationIcon(R.drawable.v_arrow_left_dark_x24);
+ recyclerView = findViewById(R.id.recycler_view);
+ tip = findViewById(R.id.tip);
+ FloatingActionButton fab = findViewById(R.id.fab);
+
+ adapter = new HostsAdapter();
+ recyclerView.setAdapter(adapter);
+ recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
+ LinearDividerItemDecoration decoration = new LinearDividerItemDecoration(
+ LinearDividerItemDecoration.VERTICAL,
+ getResources().getColor(R.color.divider),
+ LayoutUtils.dp2pix(this, 1));
+ decoration.setShowLastDivider(true);
+ recyclerView.addItemDecoration(decoration);
+ recyclerView.setSelector(Ripple.generateRippleDrawable(this, false));
+ recyclerView.setHasFixedSize(true);
+ recyclerView.setOnItemClickListener(this);
+ recyclerView.setPadding(
+ recyclerView.getPaddingLeft(),
+ recyclerView.getPaddingTop(),
+ recyclerView.getPaddingRight(),
+ recyclerView.getPaddingBottom() + getResources().getDimensionPixelOffset(R.dimen.gallery_padding_bottom_fab));
+
+ fab.setOnClickListener(this);
+
+ recyclerView.setVisibility(data.isEmpty() ? View.GONE : View.VISIBLE);
+ tip.setVisibility(data.isEmpty() ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public boolean onItemClick(EasyRecyclerView easyRecyclerView, View view, int position, long id) {
+ Pair pair = data.get(position);
+ Bundle args = new Bundle();
+ args.putString(KEY_HOST, pair.first);
+ args.putString(KEY_IP, pair.second);
+
+ DialogFragment fragment = new EditHostDialogFragment();
+ fragment.setArguments(args);
+ fragment.show(getSupportFragmentManager(), DIALOG_TAG_EDIT_HOST);
+
+ return true;
+ }
+
+ @Override
+ public void onClick(View v) {
+ new AddHostDialogFragment().show(getSupportFragmentManager(), DIALOG_TAG_ADD_HOST);
+ }
+
+ private void notifyHostsChanges() {
+ data = hosts.getAll();
+ recyclerView.setVisibility(data.isEmpty() ? View.GONE : View.VISIBLE);
+ tip.setVisibility(data.isEmpty() ? View.VISIBLE : View.GONE);
+ adapter.notifyDataSetChanged();
+ }
+
+ private class HostsHolder extends RecyclerView.ViewHolder {
+
+ public final TextView host;
+ public final TextView ip;
+
+ public HostsHolder(View itemView) {
+ super(itemView);
+ host = itemView.findViewById(R.id.host);
+ ip = itemView.findViewById(R.id.ip);
+ }
+ }
+
+ private class HostsAdapter extends RecyclerView.Adapter {
+
+ private final LayoutInflater inflater = getLayoutInflater();
+
+ @NonNull
+ @Override
+ public HostsHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new HostsHolder(inflater.inflate(R.layout.item_hosts, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull HostsHolder holder, int position) {
+ Pair pair = data.get(position);
+ holder.host.setText(pair.first);
+ holder.ip.setText(pair.second);
+ }
+
+ @Override
+ public int getItemCount() {
+ return data.size();
+ }
+ }
+
+ public abstract static class HostDialogFragment extends DialogFragment {
+
+ private TextView host;
+ private TextView ip;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_hosts, null, false);
+ host = view.findViewById(R.id.host);
+ ip = view.findViewById(R.id.ip);
+
+ Bundle arguments = getArguments();
+ if (savedInstanceState == null && arguments != null) {
+ host.setText(arguments.getString(KEY_HOST));
+ ip.setText(arguments.getString(KEY_IP));
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getContext()).setView(view);
+ onCreateDialogBuilder(builder);
+ AlertDialog dialog = builder.create();
+ dialog.setOnShowListener(d -> onCreateDialog((AlertDialog) d));
+
+ return dialog;
+ }
+
+ protected abstract void onCreateDialogBuilder(AlertDialog.Builder builder);
+
+ protected abstract void onCreateDialog(AlertDialog dialog);
+
+ protected void put(AlertDialog dialog) {
+ TextView host = dialog.findViewById(R.id.host);
+ TextView ip = dialog.findViewById(R.id.ip);
+ String hostString = host.getText().toString().trim().toLowerCase(Locale.US);
+ String ipString = ip.getText().toString().trim();
+
+ if (!Hosts.isValidHost(hostString)) {
+ TextInputLayout hostInputLayout = dialog.findViewById(R.id.host_input_layout);
+ hostInputLayout.setError(getContext().getString(R.string.invalid_host));
+ return;
+ }
+
+ if (!Hosts.isValidIp(ipString)) {
+ TextInputLayout ipInputLayout = dialog.findViewById(R.id.ip_input_layout);
+ ipInputLayout.setError(getContext().getString(R.string.invalid_ip));
+ return;
+ }
+
+ HostsActivity activity = (HostsActivity) dialog.getOwnerActivity();
+ activity.hosts.put(hostString, ipString);
+ activity.notifyHostsChanges();
+
+ dialog.dismiss();
+ }
+
+ protected void delete(AlertDialog dialog) {
+ TextView host = dialog.findViewById(R.id.host);
+ String hostString = host.getText().toString().trim().toLowerCase(Locale.US);
+
+ HostsActivity activity = (HostsActivity) dialog.getOwnerActivity();
+ activity.hosts.delete(hostString);
+ activity.notifyHostsChanges();
+
+ dialog.dismiss();
+ }
+ }
+
+ public static class AddHostDialogFragment extends HostDialogFragment {
+
+ @Override
+ protected void onCreateDialogBuilder(AlertDialog.Builder builder) {
+ builder.setTitle(R.string.add_host);
+ builder.setPositiveButton(R.string.add_host_add, null);
+ }
+
+ @Override
+ protected void onCreateDialog(AlertDialog dialog) {
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> put(dialog));
+ }
+ }
+
+ public static class EditHostDialogFragment extends HostDialogFragment {
+
+ @Override
+ protected void onCreateDialogBuilder(AlertDialog.Builder builder) {
+ builder.setTitle(R.string.edit_host);
+ builder.setPositiveButton(R.string.edit_host_confirm, null);
+ builder.setNegativeButton(R.string.edit_host_delete, null);
+ }
+
+ @Override
+ protected void onCreateDialog(AlertDialog dialog) {
+ dialog.findViewById(R.id.host_input_layout).setEnabled(false);
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> put(dialog));
+ dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(v -> delete(dialog));
+ }
+ }
+}
diff --git a/app/src/main/res/layout/activity_hosts.xml b/app/src/main/res/layout/activity_hosts.xml
new file mode 100644
index 000000000..85deede4e
--- /dev/null
+++ b/app/src/main/res/layout/activity_hosts.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_hosts.xml b/app/src/main/res/layout/dialog_hosts.xml
new file mode 100644
index 000000000..6e1a18a78
--- /dev/null
+++ b/app/src/main/res/layout/dialog_hosts.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_hosts.xml b/app/src/main/res/layout/item_hosts.xml
new file mode 100644
index 000000000..98047bdff
--- /dev/null
+++ b/app/src/main/res/layout/item_hosts.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index b22fc099f..34160ccc1 100755
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -355,6 +355,19 @@
应用
点击右上角的对勾来保存设置
+
+ Hosts
+ hosts.txt
+ Host
+ IP
+ 添加 host
+ 添加
+ 编辑 host
+ 确认
+ 删除
+ 无效的 host
+ 无效的 IP
+
第 %d 页
@string/refresh
@@ -480,6 +493,8 @@
清理内存缓存
清理缩略图、画廊信息的内存缓存
阅读缓存大小
+ Hosts
+ 将主机名称映射到相应的IP地址
图案保护
未设置图案保护
已设置图案保护
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 24d4454e0..9d1b59ed7 100755
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -355,6 +355,19 @@
應用
點擊右上角的對勾來保存設置
+
+ Hosts
+ hosts.txt
+ Host
+ IP
+ 添加 host
+ 添加
+ 編輯 host
+ 確認
+ 刪除
+ 無效的 host
+ 無效的 IP
+
第 %d 頁
@string/refresh
@@ -480,6 +493,8 @@
清理內存緩存
清理縮略圖、畫廊信息的內存緩存
閲讀緩存大小
+ Hosts
+ 將主機名稱對映到相應的IP位址
圖案保護
未設置圖案保護
已設置圖案保護
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 66cbc724b..c0551d44d 100755
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -355,6 +355,19 @@
應用
點選右上角的對勾來儲存設定
+
+ Hosts
+ hosts.txt
+ Host
+ IP
+ 添加 host
+ 添加
+ 編輯 host
+ 確認
+ 刪除
+ 無效的 host
+ 無效的 IP
+
第 %d 頁
@string/refresh
@@ -480,6 +493,8 @@
清除記憶體快取
清除縮圖、圖庫詳細資料的記憶體快取
閱讀快取大小
+ Hosts
+ 將主機名稱對映到相應的IP位址
繪圖解鎖
尚未設定繪圖解鎖
已設定繪圖解鎖
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a85315737..b60a22280 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -399,6 +399,19 @@
Apply
Click the check mark to save the settings
+
+ Hosts
+ hosts.txt
+ Host
+ IP
+ Add a host
+ Add
+ Edit the host
+ Confirm
+ Delete
+ Invalid host
+ Invalid IP
+
Page %d
@string/refresh
@@ -523,6 +536,8 @@
Clear memory cache
Clear thumb, gallery info memory cache
Read cache size
+ Hosts
+ Map hostnames to IP addresses
Pattern protection
Pattern protection has not been set
Pattern protection has been set
diff --git a/app/src/main/res/xml/advanced_settings.xml b/app/src/main/res/xml/advanced_settings.xml
index 3d57ca03c..945271f55 100644
--- a/app/src/main/res/xml/advanced_settings.xml
+++ b/app/src/main/res/xml/advanced_settings.xml
@@ -48,6 +48,13 @@
app:entryValues="@array/read_cache_size_entry_values"
android:defaultValue="160"/>
+
+