├── settings.gradle ├── screenshot.png ├── app ├── libs │ └── RootTools-4.2.jar ├── src │ └── main │ │ ├── assets │ │ └── fonts │ │ │ ├── Roboto-Light.ttf │ │ │ ├── Roboto-Italic.ttf │ │ │ └── Roboto-Regular.ttf │ │ ├── res │ │ ├── drawable-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_menu_mark.png │ │ │ ├── ic_action_edit.png │ │ │ ├── ic_action_delete.png │ │ │ ├── ic_action_search.png │ │ │ ├── ic_action_toggle.png │ │ │ ├── ic_menu_refresh.png │ │ │ ├── ic_action_add_dark.png │ │ │ └── ic_menu_info_details.png │ │ ├── drawable-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_menu_mark.png │ │ │ ├── ic_action_edit.png │ │ │ ├── ic_action_delete.png │ │ │ ├── ic_action_search.png │ │ │ ├── ic_action_toggle.png │ │ │ ├── ic_menu_refresh.png │ │ │ ├── ic_action_add_dark.png │ │ │ └── ic_menu_info_details.png │ │ ├── drawable-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_menu_mark.png │ │ │ ├── ic_action_delete.png │ │ │ ├── ic_action_edit.png │ │ │ ├── ic_action_search.png │ │ │ ├── ic_action_toggle.png │ │ │ ├── ic_menu_refresh.png │ │ │ ├── ic_action_add_dark.png │ │ │ └── ic_menu_info_details.png │ │ ├── drawable-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── attrs.xml │ │ │ ├── dimens.xml │ │ │ ├── styles.xml │ │ │ └── strings.xml │ │ ├── values-sw720dp-land │ │ │ └── dimens.xml │ │ ├── menu │ │ │ ├── add_edit_menu.xml │ │ │ ├── list_contextual_actions.xml │ │ │ └── list_menu.xml │ │ └── layout │ │ │ ├── list_hosts_fragment.xml │ │ │ ├── list_hosts_empty.xml │ │ │ ├── list_hosts_layout.xml │ │ │ ├── about_dialog.xml │ │ │ ├── checkable_host_item.xml │ │ │ └── add_edit_host_layout.xml │ │ ├── java │ │ └── com │ │ │ └── nilhcem │ │ │ └── hostseditor │ │ │ ├── event │ │ │ ├── TaskCompletedEvent.java │ │ │ ├── RefreshHostsEvent.java │ │ │ ├── StartAddEditActivityEvent.java │ │ │ ├── CreatedHostEvent.java │ │ │ └── LoadingEvent.java │ │ │ ├── core │ │ │ ├── dagger │ │ │ │ ├── HostsEditorModule.java │ │ │ │ └── HostsEditorComponent.java │ │ │ ├── log │ │ │ │ └── ReleaseTree.java │ │ │ ├── util │ │ │ │ ├── ThreadPreconditions.java │ │ │ │ ├── Compatibility.java │ │ │ │ └── InetAddresses.java │ │ │ ├── Host.java │ │ │ └── HostsManager.java │ │ │ ├── ui │ │ │ ├── widget │ │ │ │ ├── TypefacedButton.java │ │ │ │ ├── TypefacedTextView.java │ │ │ │ ├── InertCheckBox.java │ │ │ │ └── CheckableHostItem.java │ │ │ ├── BaseActivity.java │ │ │ ├── about │ │ │ │ └── AboutDialogFragment.java │ │ │ ├── list │ │ │ │ ├── ListHostsSearchFilter.java │ │ │ │ ├── ListHostsAdapter.java │ │ │ │ ├── ListHostsActivity.java │ │ │ │ └── ListHostsFragment.java │ │ │ ├── BaseFragment.java │ │ │ └── addedit │ │ │ │ ├── AddEditHostActivity.java │ │ │ │ └── AddEditHostFragment.java │ │ │ ├── task │ │ │ ├── ToggleHostsAsync.java │ │ │ ├── RemoveHostsAsync.java │ │ │ ├── AddEditHostAsync.java │ │ │ ├── ListHostsAsync.java │ │ │ └── GenericTaskAsync.java │ │ │ └── HostsEditorApplication.java │ │ └── AndroidManifest.xml └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradlew.bat ├── gradlew ├── LICENSE.txt └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/screenshot.png -------------------------------------------------------------------------------- /app/libs/RootTools-4.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/libs/RootTools-4.2.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/assets/fonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/assets/fonts/Roboto-Italic.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/assets/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_menu_mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-hdpi/ic_menu_mark.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_menu_mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-mdpi/ic_menu_mark.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-hdpi/ic_action_edit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-mdpi/ic_action_edit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_menu_mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-xhdpi/ic_menu_mark.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-hdpi/ic_action_delete.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-hdpi/ic_action_search.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_toggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-hdpi/ic_action_toggle.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_menu_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-hdpi/ic_menu_refresh.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-mdpi/ic_action_delete.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-mdpi/ic_action_search.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_toggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-mdpi/ic_action_toggle.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_menu_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-mdpi/ic_menu_refresh.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-xhdpi/ic_action_delete.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-xhdpi/ic_action_edit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-xhdpi/ic_action_search.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_toggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-xhdpi/ic_action_toggle.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_menu_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-xhdpi/ic_menu_refresh.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_add_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-hdpi/ic_action_add_dark.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_add_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-mdpi/ic_action_add_dark.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_add_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-xhdpi/ic_action_add_dark.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_menu_info_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-hdpi/ic_menu_info_details.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_menu_info_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-mdpi/ic_menu_info_details.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_menu_info_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/HEAD/app/src/main/res/drawable-xhdpi/ic_menu_info_details.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #fff3f3f3 4 | #ff909090 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jul 13 13:14:00 GMT 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/res/values-sw720dp-land/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 128dp 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/menu/add_edit_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/event/TaskCompletedEvent.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.event; 2 | 3 | public class TaskCompletedEvent { 4 | 5 | public final String tag; 6 | public final boolean isSuccessful; 7 | 8 | public TaskCompletedEvent(String pTag, boolean pIsSuccessful) { 9 | tag = pTag; 10 | isSuccessful = pIsSuccessful; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/core/dagger/HostsEditorModule.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.core.dagger; 2 | 3 | import com.squareup.otto.Bus; 4 | 5 | import javax.inject.Singleton; 6 | 7 | import dagger.Module; 8 | import dagger.Provides; 9 | 10 | @Module 11 | public class HostsEditorModule { 12 | 13 | @Provides @Singleton Bus provideBus() { 14 | return new Bus(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16dp 5 | 16dp 6 | 7 | 8 | 20dp 9 | 16sp 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/event/RefreshHostsEvent.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.event; 2 | 3 | import com.nilhcem.hostseditor.core.Host; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Sent to the bus when hosts need to be refreshed. 9 | */ 10 | public class RefreshHostsEvent { 11 | 12 | public final List hosts; 13 | 14 | public RefreshHostsEvent(List refreshedHosts) { 15 | hosts = refreshedHosts; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/event/StartAddEditActivityEvent.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.event; 2 | 3 | import com.nilhcem.hostseditor.core.Host; 4 | 5 | public class StartAddEditActivityEvent { 6 | 7 | /** 8 | * The Host entry to modify (edit mode), or {@code null} (add mode). 9 | */ 10 | public final Host host; 11 | 12 | public StartAddEditActivityEvent(Host addEditHost) { 13 | host = addEditHost; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/core/log/ReleaseTree.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.core.log; 2 | 3 | import android.util.Log; 4 | 5 | import timber.log.Timber; 6 | 7 | public class ReleaseTree extends Timber.Tree { 8 | 9 | @Override 10 | protected void log(int priority, String tag, String message, Throwable t) { 11 | // Only log ERROR, and WTF. 12 | if (priority > Log.WARN) { 13 | Log.println(priority, tag, message); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/ui/widget/TypefacedButton.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.ui.widget; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.widget.Button; 6 | 7 | /** 8 | * Button using a custom font. 9 | * 10 | * @see TypefacedTextView 11 | */ 12 | public final class TypefacedButton extends Button { 13 | 14 | public TypefacedButton(Context context, AttributeSet attrs) { 15 | super(context, attrs); 16 | TypefacedTextView.applyFont(context, attrs, this); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary files 2 | *~ 3 | \#* 4 | tmp/ 5 | 6 | # Build directory 7 | build/ 8 | 9 | # Eclipse files 10 | .metadata 11 | .pmd 12 | .apt_generated/ 13 | 14 | # IntelliJ files 15 | .idea 16 | *.iml 17 | 18 | # Android built application files 19 | *.apk 20 | *.ap_ 21 | 22 | # Files for the dex VM 23 | *.dex 24 | 25 | # Java class files 26 | *.class 27 | 28 | # Generated files 29 | bin/ 30 | gen/ 31 | 32 | # Local configuration file (sdk path, etc) 33 | local.properties 34 | 35 | # OSX files 36 | .DS_Store 37 | 38 | # Windows files 39 | Thumbs.db 40 | 41 | # Gradle 42 | .gradle 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/event/CreatedHostEvent.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.event; 2 | 3 | import com.nilhcem.hostseditor.core.Host; 4 | 5 | public class CreatedHostEvent { 6 | 7 | /** 8 | * Host (add mode), or the modified version of the Host entry (edit mode). 9 | */ 10 | public final Host editedHost; 11 | 12 | /** 13 | * Original version of the Host entry (edit mode) or {@code null} (add mode). 14 | */ 15 | public final Host originalHost; 16 | 17 | public CreatedHostEvent(Host original, Host edited) { 18 | originalHost = original; 19 | editedHost = edited; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/core/util/ThreadPreconditions.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.core.util; 2 | 3 | import android.os.Looper; 4 | 5 | import com.nilhcem.hostseditor.BuildConfig; 6 | 7 | public final class ThreadPreconditions { 8 | 9 | private ThreadPreconditions() { 10 | throw new UnsupportedOperationException(); 11 | } 12 | 13 | public static void checkOnMainThread() { 14 | if (BuildConfig.DEBUG) { 15 | if (Thread.currentThread() != Looper.getMainLooper().getThread()) { 16 | throw new IllegalStateException("This method should be called from the Main Thread"); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 14 | 15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_hosts_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 14 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/core/dagger/HostsEditorComponent.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.core.dagger; 2 | 3 | import com.nilhcem.hostseditor.task.GenericTaskAsync; 4 | import com.nilhcem.hostseditor.task.ListHostsAsync; 5 | import com.nilhcem.hostseditor.ui.BaseActivity; 6 | import com.nilhcem.hostseditor.ui.addedit.AddEditHostFragment; 7 | import com.nilhcem.hostseditor.ui.list.ListHostsFragment; 8 | 9 | import javax.inject.Singleton; 10 | 11 | import dagger.Component; 12 | 13 | @Singleton 14 | @Component( 15 | modules = { 16 | HostsEditorModule.class 17 | } 18 | ) 19 | public interface HostsEditorComponent { 20 | 21 | void inject(BaseActivity activity); 22 | 23 | void inject(GenericTaskAsync async); 24 | 25 | void inject(AddEditHostFragment fragment); 26 | 27 | void inject(ListHostsAsync async); 28 | 29 | void inject(ListHostsFragment fragment); 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/task/ToggleHostsAsync.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.task; 2 | 3 | import android.content.Context; 4 | 5 | import com.nilhcem.hostseditor.R; 6 | import com.nilhcem.hostseditor.core.Host; 7 | 8 | import timber.log.Timber; 9 | 10 | /** 11 | * AsyncTask that toggles one or many host entries and triggers a {@code TaskCompletedEvent} event. 12 | */ 13 | public class ToggleHostsAsync extends GenericTaskAsync { 14 | 15 | public ToggleHostsAsync(Context appContext, boolean flagMsg) { 16 | super(appContext, flagMsg); 17 | } 18 | 19 | @Override 20 | protected void process(Host... params) { 21 | Timber.d("Toggle hosts"); 22 | 23 | for (Host host : params) { 24 | host.toggleComment(); 25 | } 26 | } 27 | 28 | @Override 29 | protected int getLoadingMsgRes() { 30 | return mFlagLoadingMsg ? R.string.loading_toggle_single : R.string.loading_toggle_multiple; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/event/LoadingEvent.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.event; 2 | 3 | import android.content.Context; 4 | 5 | public class LoadingEvent { 6 | 7 | private final boolean mIsLoading; 8 | private final int mMessageRes; 9 | private final String mMessage; 10 | 11 | public LoadingEvent() { 12 | this(false, ""); 13 | } 14 | 15 | public LoadingEvent(boolean isLoading, int messageRes) { 16 | mIsLoading = isLoading; 17 | mMessageRes = messageRes; 18 | mMessage = null; 19 | } 20 | 21 | public LoadingEvent(boolean isLoading, String message) { 22 | mIsLoading = isLoading; 23 | mMessage = message; 24 | mMessageRes = 0; 25 | } 26 | 27 | public boolean isLoading() { 28 | return mIsLoading; 29 | } 30 | 31 | public String getMessage(Context context) { 32 | if (mMessage == null) { 33 | return context.getString(mMessageRes); 34 | } 35 | return mMessage; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/ui/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.ui; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.actionbarsherlock.app.SherlockFragmentActivity; 6 | import com.nilhcem.hostseditor.HostsEditorApplication; 7 | import com.squareup.otto.Bus; 8 | 9 | import javax.inject.Inject; 10 | 11 | /** 12 | * All activities should extend this for dependency injection. 13 | */ 14 | public abstract class BaseActivity extends SherlockFragmentActivity { 15 | 16 | @Inject protected Bus mBus; 17 | protected HostsEditorApplication mApp; 18 | 19 | @Override 20 | protected void onCreate(Bundle state) { 21 | super.onCreate(state); 22 | mApp = HostsEditorApplication.get(this); 23 | mApp.component().inject(this); 24 | } 25 | 26 | @Override 27 | protected void onResume() { 28 | super.onResume(); 29 | mBus.register(this); 30 | } 31 | 32 | @Override 33 | protected void onPause() { 34 | mBus.unregister(this); 35 | super.onPause(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/task/RemoveHostsAsync.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.task; 2 | 3 | import android.content.Context; 4 | 5 | import com.nilhcem.hostseditor.R; 6 | import com.nilhcem.hostseditor.core.Host; 7 | 8 | import java.util.List; 9 | 10 | import timber.log.Timber; 11 | 12 | /** 13 | * AsyncTask that removes one or many host entries and triggers a {@code TaskCompletedEvent} event. 14 | */ 15 | public class RemoveHostsAsync extends GenericTaskAsync { 16 | 17 | public RemoveHostsAsync(Context appContext, boolean flagMsg) { 18 | super(appContext, flagMsg); 19 | } 20 | 21 | @Override 22 | protected void process(Host... params) { 23 | Timber.d("Remove hosts"); 24 | 25 | List hosts = mHostsManager.getHosts(false); 26 | for (Host host : params) { 27 | hosts.remove(host); 28 | } 29 | } 30 | 31 | @Override 32 | protected int getLoadingMsgRes() { 33 | return mFlagLoadingMsg ? R.string.loading_remove_single : R.string.loading_remove_multiple; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/menu/list_contextual_actions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 20 | 21 | 22 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/task/AddEditHostAsync.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.task; 2 | 3 | import android.content.Context; 4 | 5 | import com.nilhcem.hostseditor.R; 6 | import com.nilhcem.hostseditor.core.Host; 7 | 8 | import java.util.List; 9 | 10 | import timber.log.Timber; 11 | 12 | /** 13 | * AsyncTask that inserts or edits an host entry and triggers a {@code TaskCompletedEvent} event. 14 | */ 15 | public class AddEditHostAsync extends GenericTaskAsync { 16 | 17 | public AddEditHostAsync(Context appContext, boolean flagMsg) { 18 | super(appContext, flagMsg); 19 | } 20 | 21 | @Override 22 | protected void process(Host... params) { 23 | Timber.d("Add/Edit host"); 24 | Host host = params[0]; 25 | Host original = params[1]; 26 | 27 | List hosts = mHostsManager.getHosts(false); 28 | if (original == null) { 29 | hosts.add(host); 30 | } else { 31 | hosts.get(hosts.indexOf(original)).merge(host); 32 | } 33 | } 34 | 35 | @Override 36 | protected int getLoadingMsgRes() { 37 | return mFlagLoadingMsg ? R.string.loading_add : R.string.loading_edit; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_hosts_empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 22 | 23 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/HostsEditorApplication.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.nilhcem.hostseditor.core.dagger.DaggerHostsEditorComponent; 7 | import com.nilhcem.hostseditor.core.dagger.HostsEditorComponent; 8 | import com.nilhcem.hostseditor.core.dagger.HostsEditorModule; 9 | import com.nilhcem.hostseditor.core.log.ReleaseTree; 10 | 11 | import timber.log.Timber; 12 | 13 | public class HostsEditorApplication extends Application { 14 | 15 | private HostsEditorComponent mComponent; 16 | 17 | public static HostsEditorApplication get(Context context) { 18 | return (HostsEditorApplication) context.getApplicationContext(); 19 | } 20 | 21 | @Override 22 | public void onCreate() { 23 | super.onCreate(); 24 | initLogger(); 25 | initGraph(); 26 | } 27 | 28 | public HostsEditorComponent component() { 29 | return mComponent; 30 | } 31 | 32 | private void initLogger() { 33 | if (BuildConfig.DEBUG) { 34 | Timber.plant(new Timber.DebugTree()); 35 | } else { 36 | Timber.plant(new ReleaseTree()); 37 | } 38 | } 39 | 40 | private void initGraph() { 41 | mComponent = DaggerHostsEditorComponent.builder() 42 | .hostsEditorModule(new HostsEditorModule()) 43 | .build(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/ui/about/AboutDialogFragment.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.ui.about; 2 | 3 | import android.content.Intent; 4 | import android.net.Uri; 5 | import android.os.Bundle; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import com.actionbarsherlock.app.SherlockDialogFragment; 11 | import com.nilhcem.hostseditor.R; 12 | 13 | import butterknife.ButterKnife; 14 | import butterknife.OnClick; 15 | 16 | public class AboutDialogFragment extends SherlockDialogFragment { 17 | 18 | public static final String TAG = AboutDialogFragment.class.getSimpleName(); 19 | private static final String GITHUB_URL = "https://github.com/Nilhcem/hosts-editor-android"; 20 | 21 | @Override 22 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 23 | View view = inflater.inflate(R.layout.about_dialog, container, false); 24 | ButterKnife.bind(this, view); 25 | getDialog().setTitle(R.string.about_title); 26 | return view; 27 | } 28 | 29 | @Override 30 | public void onDestroyView() { 31 | ButterKnife.unbind(this); 32 | super.onDestroyView(); 33 | } 34 | 35 | @OnClick(R.id.aboutGitHub) 36 | void openGitHubUrl() { 37 | Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(GITHUB_URL)); 38 | startActivity(browserIntent); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/res/menu/list_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 18 | 19 | 20 | 25 | 26 | 27 | 32 | 33 | 34 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_hosts_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 18 | 19 | 20 | 26 | 27 | 28 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/ui/list/ListHostsSearchFilter.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.ui.list; 2 | 3 | import android.text.TextUtils; 4 | import android.widget.Filter; 5 | 6 | import com.nilhcem.hostseditor.core.Host; 7 | import com.nilhcem.hostseditor.core.HostsManager; 8 | import com.nilhcem.hostseditor.event.RefreshHostsEvent; 9 | import com.squareup.otto.Bus; 10 | 11 | import java.util.List; 12 | 13 | import javax.inject.Inject; 14 | 15 | import timber.log.Timber; 16 | 17 | public class ListHostsSearchFilter extends Filter { 18 | 19 | private final Bus mBus; 20 | private final HostsManager mHostsManager; 21 | 22 | @Inject public ListHostsSearchFilter(Bus bus, HostsManager hostsManager) { 23 | mBus = bus; 24 | mHostsManager = hostsManager; 25 | } 26 | 27 | @SuppressWarnings("unchecked") 28 | @Override 29 | protected void publishResults(CharSequence constraint, FilterResults results) { 30 | if (!TextUtils.isEmpty(constraint)) { 31 | Timber.d("Publishing result for: %s", constraint); 32 | } 33 | mBus.post(new RefreshHostsEvent((List) results.values)); 34 | } 35 | 36 | @Override 37 | protected FilterResults performFiltering(CharSequence constraint) { 38 | if (!TextUtils.isEmpty(constraint)) { 39 | Timber.d("Perform filtering for: %s", constraint); 40 | } 41 | FilterResults results = new FilterResults(); 42 | results.values = mHostsManager.filterHosts(constraint); 43 | return results; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/ui/widget/TypefacedTextView.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.ui.widget; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Typeface; 6 | import android.util.AttributeSet; 7 | import android.widget.TextView; 8 | 9 | import com.nilhcem.hostseditor.R; 10 | 11 | /** 12 | * TextView using a custom font. 13 | * 14 | * @see "http://stackoverflow.com/questions/4395309/android-want-to-set-custom-fonts-for-whole-application-not-runtime/9199258#9199258" 15 | */ 16 | public final class TypefacedTextView extends TextView { 17 | 18 | public TypefacedTextView(Context context, AttributeSet attrs) { 19 | super(context, attrs); 20 | 21 | // Typeface.createFromAsset doesn't work in the layout editor. Skipping... 22 | if (isInEditMode()) { 23 | return; 24 | } 25 | 26 | TypefacedTextView.applyFont(context, attrs, this); 27 | } 28 | 29 | static void applyFont(Context context, AttributeSet attrs, TextView view) { 30 | TypedArray styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.TypefacedTextView); 31 | if (styledAttrs != null) { 32 | String fontName = styledAttrs.getString(R.styleable.TypefacedTextView_typeface); 33 | styledAttrs.recycle(); 34 | 35 | if (fontName != null) { 36 | Typeface typeface = Typeface.createFromAsset(context.getAssets(), fontName); 37 | view.setTypeface(typeface); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'com.neenbedankt.android-apt' 3 | 4 | android { 5 | compileSdkVersion 22 6 | buildToolsVersion '22.0.1' 7 | 8 | defaultConfig { 9 | applicationId 'com.nilhcem.hostseditor' 10 | minSdkVersion 8 11 | targetSdkVersion 22 12 | versionCode 5 13 | versionName '1.4' 14 | resConfig 'en' 15 | } 16 | 17 | compileOptions { 18 | sourceCompatibility JavaVersion.VERSION_1_7 19 | targetCompatibility JavaVersion.VERSION_1_7 20 | } 21 | 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | } 26 | } 27 | 28 | packagingOptions { 29 | exclude 'META-INF/LICENSE.txt' 30 | } 31 | 32 | lintOptions { 33 | abortOnError false 34 | } 35 | } 36 | 37 | dependencies { 38 | // Commons IO 39 | compile 'commons-io:commons-io:2.4' 40 | 41 | // Dagger 42 | compile 'com.google.dagger:dagger:2.0.1' 43 | apt 'com.google.dagger:dagger-compiler:2.0.1' 44 | compile 'javax.annotation:javax.annotation-api:1.2' 45 | 46 | // ActionBarSherlock + Support library 47 | compile 'com.actionbarsherlock:actionbarsherlock:4.4.0@aar' 48 | compile 'com.android.support:support-v4:22.2.0' 49 | 50 | // Butterknife 51 | compile 'com.jakewharton:butterknife:7.0.1' 52 | 53 | // Logging 54 | compile 'com.jakewharton.timber:timber:3.1.0' 55 | 56 | // Otto 57 | compile 'com.squareup:otto:1.3.8' 58 | 59 | // Other jars (RootTools) 60 | compile fileTree(dir: 'libs', include: '*.jar') 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/ui/widget/InertCheckBox.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.ui.widget; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.KeyEvent; 6 | import android.view.MotionEvent; 7 | import android.widget.CheckBox; 8 | 9 | /** 10 | * CheckBox that does not react to any user event in order to let the container handle them. 11 | */ 12 | public class InertCheckBox extends CheckBox { 13 | 14 | public InertCheckBox(Context context, AttributeSet attrs, int defStyle) { 15 | super(context, attrs, defStyle); 16 | } 17 | 18 | public InertCheckBox(Context context, AttributeSet attrs) { 19 | super(context, attrs); 20 | } 21 | 22 | public InertCheckBox(Context context) { 23 | super(context); 24 | } 25 | 26 | @Override 27 | public boolean onTouchEvent(MotionEvent event) { 28 | return false; 29 | } 30 | 31 | @Override 32 | public boolean onKeyDown(int keyCode, KeyEvent event) { 33 | return false; 34 | } 35 | 36 | @Override 37 | public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { 38 | return false; 39 | } 40 | 41 | @Override 42 | public boolean onKeyPreIme(int keyCode, KeyEvent event) { 43 | return false; 44 | } 45 | 46 | @Override 47 | public boolean onKeyShortcut(int keyCode, KeyEvent event) { 48 | return false; 49 | } 50 | 51 | @Override 52 | public boolean onKeyUp(int keyCode, KeyEvent event) { 53 | return false; 54 | } 55 | 56 | @Override 57 | public boolean onTrackballEvent(MotionEvent event) { 58 | return false; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/ui/BaseFragment.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.ui; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | 6 | import com.actionbarsherlock.app.SherlockFragment; 7 | import com.actionbarsherlock.app.SherlockFragmentActivity; 8 | import com.nilhcem.hostseditor.BuildConfig; 9 | import com.nilhcem.hostseditor.HostsEditorApplication; 10 | import com.squareup.otto.Bus; 11 | 12 | import javax.inject.Inject; 13 | 14 | /** 15 | * All fragments should extend this for dependency injection. 16 | */ 17 | public class BaseFragment extends SherlockFragment { 18 | 19 | @Inject protected Bus mBus; 20 | 21 | protected HostsEditorApplication mApp; 22 | protected SherlockFragmentActivity mActivity; 23 | 24 | @Override 25 | public void onAttach(Activity activity) { 26 | if (BuildConfig.DEBUG && !(activity instanceof SherlockFragmentActivity)) { 27 | throw new UnsupportedOperationException("Activity must be a SherlockFragmentActivity"); 28 | } 29 | 30 | super.onAttach(activity); 31 | mApp = HostsEditorApplication.get(activity); 32 | mActivity = (SherlockFragmentActivity) activity; 33 | } 34 | 35 | @Override 36 | public void onCreate(Bundle savedInstanceState) { 37 | super.onCreate(savedInstanceState); 38 | setRetainInstance(true); 39 | } 40 | 41 | @Override 42 | public void onResume() { 43 | super.onResume(); 44 | mBus.register(this); 45 | } 46 | 47 | @Override 48 | public void onPause() { 49 | mBus.unregister(this); 50 | super.onPause(); 51 | } 52 | 53 | @Override 54 | public void onDetach() { 55 | mApp = null; 56 | mActivity = null; 57 | super.onDetach(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/res/layout/about_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 16 | 17 | 18 | 24 | 25 | 26 | 34 | 35 | 36 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/task/ListHostsAsync.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.task; 2 | 3 | import android.content.Context; 4 | import android.os.AsyncTask; 5 | 6 | import com.nilhcem.hostseditor.HostsEditorApplication; 7 | import com.nilhcem.hostseditor.R; 8 | import com.nilhcem.hostseditor.core.Host; 9 | import com.nilhcem.hostseditor.core.HostsManager; 10 | import com.nilhcem.hostseditor.event.LoadingEvent; 11 | import com.nilhcem.hostseditor.event.RefreshHostsEvent; 12 | import com.squareup.otto.Bus; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | import javax.inject.Inject; 18 | 19 | /** 20 | * AsyncTask that gets all valid hosts and triggers a {@code RefreshHostEvent} event. 21 | */ 22 | public class ListHostsAsync extends AsyncTask> { 23 | 24 | @Inject Bus mBus; 25 | @Inject HostsManager mHostsManager; 26 | 27 | public ListHostsAsync(Context context) { 28 | HostsEditorApplication.get(context).component().inject(this); 29 | } 30 | 31 | @Override 32 | protected void onPreExecute() { 33 | super.onPreExecute(); 34 | mBus.post(new LoadingEvent(true, R.string.loading_hosts)); 35 | } 36 | 37 | @Override 38 | protected List doInBackground(Boolean... params) { 39 | Boolean forceRefresh = params[0]; 40 | if (forceRefresh == null) { 41 | forceRefresh = false; 42 | } 43 | 44 | List allHosts = mHostsManager.getHosts(forceRefresh); 45 | 46 | // Filter to get only valid hosts 47 | List validHosts = new ArrayList<>(); 48 | for (Host host : allHosts) { 49 | if (host.isValid()) { 50 | validHosts.add(host); 51 | } 52 | } 53 | return validHosts; 54 | } 55 | 56 | @Override 57 | protected void onPostExecute(List result) { 58 | super.onPostExecute(result); 59 | mBus.post(new RefreshHostsEvent(result)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/task/GenericTaskAsync.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.task; 2 | 3 | import android.content.Context; 4 | import android.os.AsyncTask; 5 | 6 | import com.nilhcem.hostseditor.HostsEditorApplication; 7 | import com.nilhcem.hostseditor.core.Host; 8 | import com.nilhcem.hostseditor.core.HostsManager; 9 | import com.nilhcem.hostseditor.event.LoadingEvent; 10 | import com.nilhcem.hostseditor.event.TaskCompletedEvent; 11 | import com.squareup.otto.Bus; 12 | 13 | import javax.inject.Inject; 14 | 15 | import timber.log.Timber; 16 | 17 | public abstract class GenericTaskAsync extends AsyncTask { 18 | 19 | @Inject Bus mBus; 20 | @Inject HostsManager mHostsManager; 21 | 22 | private Context mAppContext; 23 | protected boolean mFlagLoadingMsg; // which loading message (between 2) to display: (singular/plural) - (add/edit). 24 | 25 | public GenericTaskAsync(Context appContext, boolean flagMsg) { 26 | mAppContext = appContext; 27 | mFlagLoadingMsg = flagMsg; 28 | HostsEditorApplication.get(appContext).component().inject(this); 29 | } 30 | 31 | @Override 32 | protected void onPreExecute() { 33 | super.onPreExecute(); 34 | mBus.post(new LoadingEvent(true, getLoadingMsgRes())); 35 | } 36 | 37 | @Override 38 | protected Void doInBackground(Host... params) { 39 | process(params); 40 | if (!mHostsManager.saveHosts(mAppContext)) { 41 | cancel(false); 42 | } 43 | return null; 44 | } 45 | 46 | @Override 47 | protected void onPostExecute(Void result) { 48 | super.onPostExecute(result); 49 | Timber.d("Task fully executed"); 50 | mBus.post(new TaskCompletedEvent(getClass().getSimpleName(), true)); 51 | } 52 | 53 | @Override 54 | protected void onCancelled() { 55 | super.onCancelled(); 56 | Timber.w("Task cancelled"); 57 | mBus.post(new TaskCompletedEvent(getClass().getSimpleName(), false)); 58 | } 59 | 60 | /** 61 | * This method should edit the main {@code List}. 62 | * 63 | * @param params selected Hosts from the main ListView. 64 | */ 65 | protected abstract void process(Host... params); 66 | 67 | protected abstract int getLoadingMsgRes(); 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/core/util/Compatibility.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.core.util; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.res.Resources; 6 | import android.graphics.Point; 7 | import android.os.Build; 8 | import android.util.DisplayMetrics; 9 | import android.view.Display; 10 | import android.view.WindowManager; 11 | 12 | public class Compatibility { 13 | 14 | private Compatibility() { 15 | throw new UnsupportedOperationException(); 16 | } 17 | 18 | /** 19 | * Checks if current SDK is compatible with the desired API level. 20 | * 21 | * @param apiLevel the required API level. 22 | * @return {@code true} if current OS is compatible. 23 | */ 24 | private static boolean isCompatible(int apiLevel) { 25 | return android.os.Build.VERSION.SDK_INT >= apiLevel; 26 | } 27 | 28 | @SuppressWarnings("deprecation") 29 | @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) 30 | public static Point getScreenDimensions(Context context) { 31 | Point size = new Point(); 32 | 33 | WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 34 | Display display = wm.getDefaultDisplay(); 35 | if (Compatibility.isCompatible(Build.VERSION_CODES.HONEYCOMB_MR2)) { 36 | display.getSize(size); 37 | } else { 38 | size.set(display.getWidth(), display.getHeight()); 39 | } 40 | return size; 41 | } 42 | 43 | /** 44 | * This method converts dp unit to equivalent device specific value in pixels. 45 | * 46 | * @param dp A value in dp (Device independent pixels) unit. Which we need to convert into pixels. 47 | * @param context Context to get resources and device specific display metrics. 48 | * @return A float value to represent Pixels equivalent to dp according to device. 49 | */ 50 | public static float convertDpToPixel(float dp, Context context) { 51 | Resources resources = context.getResources(); 52 | DisplayMetrics metrics = resources.getDisplayMetrics(); 53 | return dp * (metrics.densityDpi / 160f); 54 | } 55 | 56 | public static int convertDpToIntPixel(float dp, Context context) { 57 | return Math.round(convertDpToPixel(dp, context)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/res/layout/checkable_host_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 23 | 24 | 25 | 33 | 34 | 35 | 43 | 44 | 45 | 55 | 56 | 57 | 58 | 65 | 66 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/ui/widget/CheckableHostItem.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.ui.widget; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.text.TextUtils; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.widget.Checkable; 9 | import android.widget.RelativeLayout; 10 | import android.widget.TextView; 11 | 12 | import com.nilhcem.hostseditor.R; 13 | import com.nilhcem.hostseditor.core.Host; 14 | 15 | import java.util.Locale; 16 | 17 | import butterknife.Bind; 18 | import butterknife.ButterKnife; 19 | 20 | /** 21 | * Custom component that implements the Checkable interface. 22 | * 23 | * @see "http://www.marvinlabs.com/2010/10/29/custom-listview-ability-check-items/" 24 | */ 25 | public class CheckableHostItem extends RelativeLayout implements Checkable { 26 | 27 | @Bind(R.id.hostItemIp) TextView mIp; 28 | @Bind(R.id.hostItemHostname) TextView mHostname; 29 | @Bind(R.id.hostItemComment) TextView mComment; 30 | @Bind(R.id.hostItemCheckbox) InertCheckBox mCheckbox; 31 | 32 | private int mTextColor; 33 | private int mCommentColor; 34 | 35 | public CheckableHostItem(Context context) { 36 | super(context); 37 | initLayout(context); 38 | } 39 | 40 | private void initLayout(Context context) { 41 | LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 42 | View view = inflater.inflate(R.layout.checkable_host_item, this, true); 43 | ButterKnife.bind(this, view); 44 | 45 | Resources res = context.getResources(); 46 | mTextColor = res.getColor(R.color.list_hosts_entry); 47 | mCommentColor = res.getColor(R.color.list_hosts_comment); 48 | } 49 | 50 | public void init(Host host, int ipMinWidth, int ipMaxWidth) { 51 | String ip = String.format(Locale.US, "%s%s", (host.isCommented() ? Host.STR_COMMENT : ""), host.getIp()); 52 | 53 | int textColor; 54 | if (host.isCommented()) { 55 | textColor = mCommentColor; 56 | } else { 57 | textColor = mTextColor; 58 | } 59 | 60 | mIp.setText(ip); 61 | mIp.setTextColor(textColor); 62 | mIp.setMinimumWidth(ipMinWidth); 63 | mIp.setMaxWidth(ipMaxWidth); 64 | mHostname.setText(host.getHostName()); 65 | mHostname.setTextColor(textColor); 66 | mCheckbox.setChecked(false); 67 | 68 | String comment = host.getComment(); 69 | if (TextUtils.isEmpty(comment)) { 70 | mComment.setVisibility(View.GONE); 71 | } else { 72 | mComment.setText(String.format(Locale.US, "%s%s", Host.STR_COMMENT, comment)); 73 | mComment.setVisibility(View.VISIBLE); 74 | } 75 | } 76 | 77 | 78 | @Override 79 | public boolean isChecked() { 80 | return mCheckbox.isChecked(); 81 | } 82 | 83 | @Override 84 | public void setChecked(boolean checked) { 85 | mCheckbox.setChecked(checked); 86 | } 87 | 88 | @Override 89 | public void toggle() { 90 | mCheckbox.toggle(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hosts Editor 4 | 5 | 6 | Search 7 | Search results 8 | Add Host 9 | Reload host entries 10 | Select all hosts 11 | About 12 | 13 | 14 | Add Hostname 15 | Edit Hostname 16 | IP Address: 17 | Hostname: 18 | Comment: 19 | Error 20 | IP address is invalid 21 | Hostname is invalid 22 | OK 23 | 24 | 25 | Add comment 26 | Remove comment 27 | 28 | 29 | No \'hosts\' records found. 30 | We couldn\'t find any host entry.\nFeel free to add some! 31 | 32 | 33 | %d selected 34 | Edit 35 | Delete 36 | Toggle 37 | 38 | 39 | Error 40 | Can\'t edit hosts file, make sure you have root/SuperUser rights.\nThe app should be allowed to request super user access. 41 | OK 42 | 43 | 44 | Delete 45 | 46 | Delete selected host? 47 | Delete selected hosts? 48 | 49 | Cancel 50 | Delete 51 | 52 | 53 | Loading host entries 54 | Adding entry 55 | Editing entry 56 | Removing entry 57 | Removing entries 58 | Toggling entry 59 | Toggling entries 60 | 61 | 62 | About Hosts Editor 63 | This app lets you modify your /etc/hosts file on Android.\nYou might need to reboot to clear your DNS cache after any change. 64 | 2013 – 2015\nhttp://www.nilhcem.com 65 | GitHub 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/ui/list/ListHostsAdapter.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.ui.list; 2 | 3 | import android.content.Context; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.AbsListView.LayoutParams; 7 | import android.widget.BaseAdapter; 8 | import android.widget.Filter; 9 | import android.widget.Filterable; 10 | 11 | import com.nilhcem.hostseditor.core.Host; 12 | import com.nilhcem.hostseditor.core.util.Compatibility; 13 | import com.nilhcem.hostseditor.core.util.ThreadPreconditions; 14 | import com.nilhcem.hostseditor.ui.widget.CheckableHostItem; 15 | 16 | import java.util.Collections; 17 | import java.util.List; 18 | 19 | import javax.inject.Inject; 20 | 21 | import timber.log.Timber; 22 | 23 | public class ListHostsAdapter extends BaseAdapter implements Filterable { 24 | 25 | private final ListHostsSearchFilter mSearchFilter; 26 | 27 | private List mHosts = Collections.emptyList(); 28 | private Context mAppContext; 29 | 30 | private int mIpMinWidth; 31 | private int mIpMaxWidth; 32 | 33 | @Inject public ListHostsAdapter(ListHostsSearchFilter searchFilter) { 34 | mSearchFilter = searchFilter; 35 | } 36 | 37 | public void init(Context appContext) { 38 | mAppContext = appContext; 39 | } 40 | 41 | public void updateHosts(List hosts) { 42 | ThreadPreconditions.checkOnMainThread(); 43 | if (hosts == null) { 44 | mHosts = Collections.emptyList(); 45 | } else { 46 | mHosts = hosts; 47 | } 48 | notifyDataSetChanged(); 49 | } 50 | 51 | public void computeViewWidths(Context context) { 52 | // The IP column can be very large, especially when it holds ipv6 with 39 chars. 53 | // Its size has to be generated programmatically, as it should fit with the device's width. 54 | // A tablet can afford having a large column but not a phone. 55 | 56 | int screenWidth = Compatibility.getScreenDimensions(context).x; 57 | Timber.d("Screen width: %d", screenWidth); 58 | 59 | // 1: Compute minimum width. 60 | // Min width must be between [100dp, 160dp]. If possible, 30% of screen width. 61 | int minWidth = screenWidth * 30 / 100; 62 | int minRange = Compatibility.convertDpToIntPixel(100f, context); 63 | int maxRange = Compatibility.convertDpToIntPixel(160f, context); 64 | 65 | if (minWidth < minRange) { 66 | minWidth = minRange; 67 | } 68 | if (minWidth > maxRange) { 69 | minWidth = maxRange; 70 | } 71 | 72 | // 2: Compute maximum width, usually 35% of screen width. 73 | int maxWidth = screenWidth * 35 / 100; 74 | if (maxWidth < minWidth) { 75 | maxWidth = minWidth; 76 | } 77 | 78 | Timber.d("Min width: %d - Max width: %d", minWidth, maxWidth); 79 | mIpMinWidth = minWidth; 80 | mIpMaxWidth = maxWidth; 81 | } 82 | 83 | @Override 84 | public int getCount() { 85 | return mHosts.size(); 86 | } 87 | 88 | @Override 89 | public Host getItem(int position) { 90 | return mHosts.get(position); 91 | } 92 | 93 | @Override 94 | public long getItemId(int position) { 95 | return position; 96 | } 97 | 98 | @Override 99 | public View getView(int position, View convertView, ViewGroup parent) { 100 | CheckableHostItem view; 101 | 102 | if (convertView == null) { 103 | view = new CheckableHostItem(mAppContext); 104 | } else { 105 | view = (CheckableHostItem) convertView; 106 | } 107 | 108 | Host host = getItem(position); 109 | view.init(host, mIpMinWidth, mIpMaxWidth); 110 | return view; 111 | } 112 | 113 | @Override 114 | public Filter getFilter() { 115 | return mSearchFilter; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/res/layout/add_edit_host_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 17 | 18 | 19 | 25 | 26 | 27 | 35 | 36 | 37 | 38 | 39 | 40 | 46 | 47 | 48 | 55 | 56 | 57 | 66 | 67 | 68 | 77 | 78 | 79 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/ui/addedit/AddEditHostActivity.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.ui.addedit; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v4.app.FragmentManager; 6 | import android.support.v4.app.FragmentTransaction; 7 | 8 | import com.actionbarsherlock.app.ActionBar; 9 | import com.actionbarsherlock.view.Menu; 10 | import com.actionbarsherlock.view.MenuItem; 11 | import com.nilhcem.hostseditor.R; 12 | import com.nilhcem.hostseditor.core.Host; 13 | import com.nilhcem.hostseditor.event.CreatedHostEvent; 14 | import com.nilhcem.hostseditor.ui.BaseActivity; 15 | import com.nilhcem.hostseditor.ui.list.ListHostsActivity; 16 | import com.squareup.otto.Subscribe; 17 | 18 | public class AddEditHostActivity extends BaseActivity { 19 | 20 | public static final String EXTRA_HOST_ORIGINAL = "hostOriginal"; // useful in Edit mode, this is the original Host entry before being edited. 21 | public static final String EXTRA_HOST_MODIFIED = "hostModified"; // the new Host entry, after having been edited. 22 | 23 | private AddEditHostFragment mFragment; 24 | 25 | @Override 26 | protected void onCreate(Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | 29 | Host hostToEdit = getHostFromIntent(); 30 | initActionBar(hostToEdit); 31 | 32 | // Add fragment 33 | FragmentManager fm = getSupportFragmentManager(); 34 | mFragment = (AddEditHostFragment) fm.findFragmentByTag(AddEditHostFragment.TAG); 35 | if (mFragment == null) { 36 | mFragment = AddEditHostFragment.newInstance(hostToEdit); 37 | FragmentTransaction ft = fm.beginTransaction(); 38 | ft.replace(android.R.id.content, mFragment, AddEditHostFragment.TAG); 39 | ft.commit(); 40 | } 41 | } 42 | 43 | @Override 44 | public boolean onCreateOptionsMenu(Menu menu) { 45 | getSupportMenuInflater().inflate(R.menu.add_edit_menu, menu); 46 | return true; 47 | } 48 | 49 | @Override 50 | public boolean onPrepareOptionsMenu(Menu menu) { 51 | MenuItem item = menu.findItem(R.id.action_add_rm_comment); 52 | if (mFragment.hasComment()) { 53 | item.setTitle(R.string.action_remove_comment); 54 | } else { 55 | item.setTitle(R.string.action_add_comment); 56 | } 57 | return super.onPrepareOptionsMenu(menu); 58 | } 59 | 60 | @Override 61 | public boolean onOptionsItemSelected(MenuItem item) { 62 | switch (item.getItemId()) { 63 | case android.R.id.home: 64 | Intent intent = new Intent(this, ListHostsActivity.class); 65 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 66 | startActivity(intent); 67 | return true; 68 | case R.id.action_add_rm_comment: 69 | mFragment.toggleCommentVisibility(); 70 | return true; 71 | default: 72 | return super.onOptionsItemSelected(item); 73 | } 74 | } 75 | 76 | @Subscribe 77 | public void onHostCreatedFromFragment(CreatedHostEvent event) { 78 | Intent returnIntent = new Intent(); 79 | returnIntent.putExtra(EXTRA_HOST_ORIGINAL, event.originalHost); 80 | returnIntent.putExtra(EXTRA_HOST_MODIFIED, event.editedHost); 81 | setResult(RESULT_OK, returnIntent); 82 | finish(); 83 | } 84 | 85 | private Host getHostFromIntent() { 86 | Host host = null; 87 | 88 | Bundle bundle = getIntent().getExtras(); 89 | if (bundle != null) { 90 | host = bundle.getParcelable(EXTRA_HOST_ORIGINAL); 91 | } 92 | return host; 93 | } 94 | 95 | private void initActionBar(Host hostToEdit) { 96 | int titleRes; 97 | if (hostToEdit == null) { 98 | titleRes = R.string.add_host_title; 99 | } else { 100 | titleRes = R.string.edit_host_title; 101 | } 102 | 103 | ActionBar actionBar = getSupportActionBar(); 104 | actionBar.setDisplayHomeAsUpEnabled(true); 105 | actionBar.setDisplayShowTitleEnabled(true); 106 | actionBar.setTitle(titleRes); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/ui/addedit/AddEditHostFragment.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.ui.addedit; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.DialogInterface; 5 | import android.os.Bundle; 6 | import android.text.TextUtils; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.Button; 11 | import android.widget.EditText; 12 | import android.widget.TextView; 13 | 14 | import com.nilhcem.hostseditor.R; 15 | import com.nilhcem.hostseditor.core.Host; 16 | import com.nilhcem.hostseditor.core.util.InetAddresses; 17 | import com.nilhcem.hostseditor.event.CreatedHostEvent; 18 | import com.nilhcem.hostseditor.ui.BaseFragment; 19 | 20 | import java.util.regex.Pattern; 21 | 22 | import butterknife.Bind; 23 | import butterknife.ButterKnife; 24 | import butterknife.OnClick; 25 | 26 | public class AddEditHostFragment extends BaseFragment { 27 | 28 | public static final String TAG = AddEditHostFragment.class.getSimpleName(); 29 | 30 | private static final String ARG_HOST = "mInitialHost"; 31 | private static final Pattern HOSTNAME_INVALID_CHARS_PATTERN = Pattern.compile("^.*[#'\",\\\\]+.*$"); 32 | 33 | private Host mInitialHost; // "edit mode" only - null for "add mode" 34 | private AlertDialog mErrorAlert; 35 | 36 | @Bind(R.id.addEditHostIp) EditText mIp; 37 | @Bind(R.id.addEditHostName) EditText mHostName; 38 | @Bind(R.id.addEditComment) EditText mComment; 39 | @Bind(R.id.addEditCommentLabel) TextView mCommentLabel; 40 | @Bind(R.id.addEditHostButton) Button mButton; 41 | 42 | public static AddEditHostFragment newInstance(Host hostToEdit) { 43 | AddEditHostFragment fragment = new AddEditHostFragment(); 44 | 45 | if (hostToEdit != null) { 46 | Bundle args = new Bundle(); 47 | args.putParcelable(ARG_HOST, hostToEdit); 48 | fragment.setArguments(args); 49 | } 50 | return fragment; 51 | } 52 | 53 | @Override 54 | public void onCreate(Bundle savedInstanceState) { 55 | super.onCreate(savedInstanceState); 56 | mApp.component().inject(this); 57 | Bundle arguments = getArguments(); 58 | if (arguments != null) { 59 | mInitialHost = arguments.getParcelable(ARG_HOST); 60 | } 61 | } 62 | 63 | @Override 64 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 65 | View view = inflater.inflate(R.layout.add_edit_host_layout, container, false); 66 | ButterKnife.bind(this, view); 67 | 68 | if (mInitialHost == null) { 69 | mButton.setText(R.string.add_host_title); 70 | } else { 71 | mIp.setText(mInitialHost.getIp()); 72 | mHostName.setText(mInitialHost.getHostName()); 73 | mButton.setText(R.string.edit_host_title); 74 | 75 | String comment = mInitialHost.getComment(); 76 | if (!TextUtils.isEmpty(comment)) { 77 | mComment.setText(comment); 78 | toggleCommentVisibility(); 79 | } 80 | } 81 | return view; 82 | } 83 | 84 | @Override 85 | public void onStop() { 86 | if (mErrorAlert != null) { 87 | mErrorAlert.dismiss(); 88 | } 89 | super.onStop(); 90 | } 91 | 92 | @Override 93 | public void onDestroyView() { 94 | ButterKnife.unbind(this); 95 | super.onDestroyView(); 96 | } 97 | 98 | @OnClick(R.id.addEditHostButton) 99 | void onAddEditHostButtonClicked(Button button) { 100 | if (button.getId() == R.id.addEditHostButton) { 101 | String ip = mIp.getText().toString(); 102 | String hostname = mHostName.getText().toString(); 103 | String comment = mComment.getText().toString(); 104 | if (TextUtils.isEmpty(comment)) { 105 | comment = null; 106 | } 107 | int error = checkFormErrors(ip, hostname); 108 | 109 | if (error == 0) { 110 | Host edited = new Host(ip, hostname, comment, false, true); 111 | mBus.post(new CreatedHostEvent(mInitialHost, edited)); 112 | } else { 113 | mErrorAlert = new AlertDialog.Builder(mActivity) 114 | .setTitle(R.string.add_edit_error_title) 115 | .setMessage(error) 116 | .setCancelable(true) 117 | .setNeutralButton(R.string.add_edit_error_ok, new DialogInterface.OnClickListener() { 118 | @Override 119 | public void onClick(DialogInterface dialog, int which) { 120 | // Do nothing 121 | } 122 | }) 123 | .create(); 124 | mErrorAlert.show(); 125 | } 126 | } 127 | } 128 | 129 | public boolean hasComment() { 130 | return mComment.getVisibility() == View.VISIBLE; 131 | } 132 | 133 | public void toggleCommentVisibility() { 134 | int visibility; 135 | if (hasComment()) { 136 | visibility = View.GONE; 137 | mComment.setText(""); 138 | } else { 139 | visibility = View.VISIBLE; 140 | } 141 | 142 | mComment.setVisibility(visibility); 143 | mCommentLabel.setVisibility(visibility); 144 | } 145 | 146 | private int checkFormErrors(String ip, String hostname) { 147 | int error = 0; 148 | 149 | if (TextUtils.isEmpty(hostname) || HOSTNAME_INVALID_CHARS_PATTERN.matcher(hostname).matches()) { 150 | error = R.string.add_edit_error_hostname; 151 | } 152 | if (TextUtils.isEmpty(ip) || !InetAddresses.isInetAddress(ip)) { 153 | error = R.string.add_edit_error_ip; 154 | } 155 | return error; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/core/Host.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.core; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | import android.text.TextUtils; 6 | 7 | import com.nilhcem.hostseditor.core.util.InetAddresses; 8 | 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | public class Host implements Parcelable { 13 | 14 | public static final String STR_COMMENT = "#"; 15 | private static final String STR_SEPARATOR = " "; 16 | private static final String HOST_PATTERN_STR = "^\\s*(" + STR_COMMENT + "?)\\s*(\\S*)\\s*([^" + STR_COMMENT + "]*)" + STR_COMMENT + "?(.*)$"; 17 | private static final Pattern HOST_PATTERN = Pattern.compile(HOST_PATTERN_STR); 18 | 19 | private String mIp; 20 | private String mHostName; 21 | private String mComment; // When host entry has a comment at the end, eg: "::1 localhost #myhome" 22 | private boolean mIsCommented; // When host entry starts with #, eg: "#::1 localhost" 23 | private boolean mIsValid; // When host entry has a valid IP + hostname 24 | 25 | public Host(String ip, String hostName, String comment, boolean isCommented, boolean isValid) { 26 | mIp = ip; 27 | mHostName = hostName; 28 | mComment = comment; 29 | mIsCommented = isCommented; 30 | mIsValid = isValid; 31 | } 32 | 33 | public void merge(Host src) { 34 | mIp = src.getIp(); 35 | mHostName = src.getHostName(); 36 | mComment = src.getComment(); 37 | mIsCommented = src.isCommented(); 38 | mIsValid = src.isValid(); 39 | } 40 | 41 | public String getIp() { 42 | return mIp; 43 | } 44 | 45 | public String getHostName() { 46 | return mHostName; 47 | } 48 | 49 | public String getComment() { 50 | return mComment; 51 | } 52 | 53 | public boolean isValid() { 54 | return mIsValid; 55 | } 56 | 57 | public boolean isCommented() { 58 | return mIsCommented; 59 | } 60 | 61 | public void toggleComment() { 62 | mIsCommented = !mIsCommented; 63 | } 64 | 65 | @Override 66 | public String toString() { 67 | StringBuilder sb = new StringBuilder(); 68 | 69 | if (mIsCommented) { 70 | sb.append(STR_COMMENT); 71 | } 72 | if (mIp != null) { 73 | sb.append(mIp).append(STR_SEPARATOR); 74 | } 75 | if (mHostName != null) { 76 | sb.append(mHostName); 77 | } 78 | if (!TextUtils.isEmpty(mComment)) { 79 | sb.append(STR_SEPARATOR).append(STR_COMMENT).append(mComment); 80 | } 81 | return sb.toString(); 82 | } 83 | 84 | public static Host fromString(String line) { 85 | Matcher matcher = HOST_PATTERN.matcher(line); 86 | String ip = null; 87 | String hostname = null; 88 | String comment = null; 89 | boolean isCommented = false; 90 | boolean isValid = false; 91 | 92 | if (matcher.find()) { 93 | isCommented = !TextUtils.isEmpty(matcher.group(1)); 94 | ip = matcher.group(2); 95 | hostname = matcher.group(3).trim(); 96 | comment = matcher.group(4).trim(); 97 | if (TextUtils.isEmpty(comment)) { 98 | comment = null; 99 | } 100 | 101 | isValid = !TextUtils.isEmpty(ip) && !TextUtils.isEmpty(hostname) 102 | && InetAddresses.isInetAddress(ip); 103 | } 104 | return new Host(ip, hostname, comment, isCommented, isValid); 105 | } 106 | 107 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 108 | 109 | @Override 110 | public Host createFromParcel(Parcel source) { 111 | return new Host(source); 112 | } 113 | 114 | @Override 115 | public Host[] newArray(int size) { 116 | return new Host[size]; 117 | } 118 | }; 119 | 120 | @Override 121 | public int describeContents() { 122 | return 0; 123 | } 124 | 125 | @Override 126 | public void writeToParcel(Parcel dest, int flags) { 127 | dest.writeString(mIp); 128 | dest.writeString(mHostName); 129 | dest.writeString(mComment); 130 | dest.writeByte((byte) (mIsCommented ? 1 : 0)); 131 | dest.writeByte((byte) (mIsValid ? 1 : 0)); 132 | } 133 | 134 | private Host(Parcel in) { 135 | mIp = in.readString(); 136 | mHostName = in.readString(); 137 | mComment = in.readString(); 138 | mIsCommented = in.readByte() == 1; 139 | mIsValid = in.readByte() == 1; 140 | } 141 | 142 | @Override 143 | public int hashCode() { 144 | final int prime = 31; 145 | int result = 1; 146 | result = prime * result + ((mComment == null) ? 0 : mComment.hashCode()); 147 | result = prime * result + ((mHostName == null) ? 0 : mHostName.hashCode()); 148 | result = prime * result + ((mIp == null) ? 0 : mIp.hashCode()); 149 | result = prime * result + (mIsCommented ? 1231 : 1237); 150 | result = prime * result + (mIsValid ? 1231 : 1237); 151 | return result; 152 | } 153 | 154 | @Override 155 | public boolean equals(Object obj) { 156 | if (this == obj) { 157 | return true; 158 | } 159 | if (obj == null) { 160 | return false; 161 | } 162 | if (getClass() != obj.getClass()) { 163 | return false; 164 | } 165 | Host other = (Host) obj; 166 | if (mComment == null) { 167 | if (other.mComment != null) { 168 | return false; 169 | } 170 | } else if (!mComment.equals(other.mComment)) { 171 | return false; 172 | } 173 | if (mHostName == null) { 174 | if (other.mHostName != null) { 175 | return false; 176 | } 177 | } else if (!mHostName.equals(other.mHostName)) { 178 | return false; 179 | } 180 | if (mIp == null) { 181 | if (other.mIp != null) { 182 | return false; 183 | } 184 | } else if (!mIp.equals(other.mIp)) { 185 | return false; 186 | } 187 | if (mIsCommented != other.mIsCommented) { 188 | return false; 189 | } 190 | if (mIsValid != other.mIsValid) { 191 | return false; 192 | } 193 | return true; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/core/util/InetAddresses.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2008 The Guava Authors 3 | * 4 | * NOTICE: THIS FILE WAS MODIFIED (removed all not required for the project) 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package com.nilhcem.hostseditor.core.util; 19 | 20 | import java.net.InetAddress; 21 | import java.nio.ByteBuffer; 22 | 23 | /** 24 | * Static utility methods pertaining to {@link InetAddress} instances. 25 | */ 26 | public final class InetAddresses { 27 | 28 | private static final int IPV4_PART_COUNT = 4; 29 | private static final int IPV6_PART_COUNT = 8; 30 | 31 | private InetAddresses() { 32 | } 33 | 34 | /** 35 | * Returns {@code true} if the supplied string is a valid IP string literal, 36 | * {@code false} otherwise. 37 | * 38 | * @param ipString {@code String} to evaluated as an IP string literal 39 | * @return {@code true} if the argument is a valid IP string literal 40 | */ 41 | public static boolean isInetAddress(String ipString) { 42 | return ipStringToBytes(ipString) != null; 43 | } 44 | 45 | private static byte[] ipStringToBytes(String ipString) { 46 | // Make a first pass to categorize the characters in this string. 47 | boolean hasColon = false; 48 | boolean hasDot = false; 49 | for (int i = 0; i < ipString.length(); i++) { 50 | char c = ipString.charAt(i); 51 | if (c == '.') { 52 | hasDot = true; 53 | } else if (c == ':') { 54 | if (hasDot) { 55 | return null; // Colons must not appear after dots. 56 | } 57 | hasColon = true; 58 | } else if (Character.digit(c, 16) == -1) { 59 | return null; // Everything else must be a decimal or hex digit. 60 | } 61 | } 62 | 63 | // Now decide which address family to parse. 64 | if (hasColon) { 65 | if (hasDot) { 66 | ipString = convertDottedQuadToHex(ipString); 67 | if (ipString == null) { 68 | return null; 69 | } 70 | } 71 | return textToNumericFormatV6(ipString); 72 | } else if (hasDot) { 73 | return textToNumericFormatV4(ipString); 74 | } 75 | return null; 76 | } 77 | 78 | private static byte[] textToNumericFormatV4(String ipString) { 79 | String[] address = ipString.split("\\.", IPV4_PART_COUNT + 1); 80 | if (address.length != IPV4_PART_COUNT) { 81 | return null; 82 | } 83 | 84 | byte[] bytes = new byte[IPV4_PART_COUNT]; 85 | try { 86 | for (int i = 0; i < bytes.length; i++) { 87 | bytes[i] = parseOctet(address[i]); 88 | } 89 | } catch (NumberFormatException ex) { 90 | return null; 91 | } 92 | 93 | return bytes; 94 | } 95 | 96 | private static byte[] textToNumericFormatV6(String ipString) { 97 | // An address can have [2..8] colons, and N colons make N+1 parts. 98 | String[] parts = ipString.split(":", IPV6_PART_COUNT + 2); 99 | if (parts.length < 3 || parts.length > IPV6_PART_COUNT + 1) { 100 | return null; 101 | } 102 | 103 | // Disregarding the endpoints, find "::" with nothing in between. 104 | // This indicates that a run of zeroes has been skipped. 105 | int skipIndex = -1; 106 | for (int i = 1; i < parts.length - 1; i++) { 107 | if (parts[i].length() == 0) { 108 | if (skipIndex >= 0) { 109 | return null; // Can't have more than one :: 110 | } 111 | skipIndex = i; 112 | } 113 | } 114 | 115 | int partsHi; // Number of parts to copy from above/before the "::" 116 | int partsLo; // Number of parts to copy from below/after the "::" 117 | if (skipIndex >= 0) { 118 | // If we found a "::", then check if it also covers the endpoints. 119 | partsHi = skipIndex; 120 | partsLo = parts.length - skipIndex - 1; 121 | if (parts[0].length() == 0 && --partsHi != 0) { 122 | return null; // ^: requires ^:: 123 | } 124 | if (parts[parts.length - 1].length() == 0 && --partsLo != 0) { 125 | return null; // :$ requires ::$ 126 | } 127 | } else { 128 | // Otherwise, allocate the entire address to partsHi. The endpoints 129 | // could still be empty, but parseHextet() will check for that. 130 | partsHi = parts.length; 131 | partsLo = 0; 132 | } 133 | 134 | // If we found a ::, then we must have skipped at least one part. 135 | // Otherwise, we must have exactly the right number of parts. 136 | int partsSkipped = IPV6_PART_COUNT - (partsHi + partsLo); 137 | if (!(skipIndex >= 0 ? partsSkipped >= 1 : partsSkipped == 0)) { 138 | return null; 139 | } 140 | 141 | // Now parse the hextets into a byte array. 142 | ByteBuffer rawBytes = ByteBuffer.allocate(2 * IPV6_PART_COUNT); 143 | try { 144 | for (int i = 0; i < partsHi; i++) { 145 | rawBytes.putShort(parseHextet(parts[i])); 146 | } 147 | for (int i = 0; i < partsSkipped; i++) { 148 | rawBytes.putShort((short) 0); 149 | } 150 | for (int i = partsLo; i > 0; i--) { 151 | rawBytes.putShort(parseHextet(parts[parts.length - i])); 152 | } 153 | } catch (NumberFormatException ex) { 154 | return null; 155 | } 156 | return rawBytes.array(); 157 | } 158 | 159 | private static String convertDottedQuadToHex(String ipString) { 160 | int lastColon = ipString.lastIndexOf(':'); 161 | String initialPart = ipString.substring(0, lastColon + 1); 162 | String dottedQuad = ipString.substring(lastColon + 1); 163 | byte[] quad = textToNumericFormatV4(dottedQuad); 164 | if (quad == null) { 165 | return null; 166 | } 167 | String penultimate = Integer.toHexString(((quad[0] & 0xff) << 8) 168 | | (quad[1] & 0xff)); 169 | String ultimate = Integer.toHexString(((quad[2] & 0xff) << 8) 170 | | (quad[3] & 0xff)); 171 | return initialPart + penultimate + ":" + ultimate; 172 | } 173 | 174 | private static byte parseOctet(String ipPart) { 175 | // Note: we already verified that this string contains only hex digits. 176 | int octet = Integer.parseInt(ipPart); 177 | // Disallow leading zeroes, because no clear standard exists on 178 | // whether these should be interpreted as decimal or octal. 179 | if (octet > 255 || (ipPart.startsWith("0") && ipPart.length() > 1)) { 180 | throw new NumberFormatException(); 181 | } 182 | return (byte) octet; 183 | } 184 | 185 | private static short parseHextet(String ipPart) { 186 | // Note: we already verified that this string contains only hex digits. 187 | int hextet = Integer.parseInt(ipPart, 16); 188 | if (hextet > 0xffff) { 189 | throw new NumberFormatException(); 190 | } 191 | return (short) hextet; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/core/HostsManager.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.core; 2 | 3 | import android.content.Context; 4 | 5 | import com.stericson.RootShell.RootShell; 6 | import com.stericson.RootShell.exceptions.RootDeniedException; 7 | import com.stericson.RootShell.execution.Command; 8 | import com.stericson.RootTools.RootTools; 9 | 10 | import org.apache.commons.io.FileUtils; 11 | import org.apache.commons.io.LineIterator; 12 | 13 | import java.io.File; 14 | import java.io.FileOutputStream; 15 | import java.io.IOException; 16 | import java.io.OutputStreamWriter; 17 | import java.util.ArrayList; 18 | import java.util.Collections; 19 | import java.util.List; 20 | import java.util.Locale; 21 | import java.util.concurrent.TimeoutException; 22 | 23 | import javax.inject.Inject; 24 | import javax.inject.Singleton; 25 | 26 | import timber.log.Timber; 27 | 28 | @Singleton 29 | public class HostsManager { 30 | 31 | private static final String UTF_8 = "UTF-8"; 32 | private static final String HOSTS_FILE_NAME = "hosts"; 33 | private static final String HOSTS_FILE_PATH = "/system/etc/" + HOSTS_FILE_NAME; 34 | 35 | private static final String LINE_SEPARATOR = System.getProperty("line.separator", "\n"); 36 | private static final String MOUNT_TYPE_RO = "ro"; 37 | private static final String MOUNT_TYPE_RW = "rw"; 38 | private static final String COMMAND_RM = "rm -f"; 39 | private static final String COMMAND_CHOWN = "chown 0:0"; 40 | private static final String COMMAND_CHMOD_644 = "chmod 644"; 41 | 42 | // Do not access this field directly even in the same class, use getAllHosts() instead. 43 | private List mHosts; 44 | 45 | @Inject 46 | public HostsManager() { 47 | } 48 | 49 | /** 50 | * Gets all host entries from hosts file. 51 | *

Must be in an async call.

52 | * 53 | * @param forceRefresh if we want to force using the hosts file (not the cache) 54 | * @return a list of host entries 55 | */ 56 | public synchronized List getHosts(boolean forceRefresh) { 57 | if (mHosts == null || forceRefresh) { 58 | mHosts = Collections.synchronizedList(new ArrayList()); 59 | 60 | LineIterator it = null; 61 | try { 62 | it = FileUtils.lineIterator(new File(HOSTS_FILE_PATH), UTF_8); 63 | while (it.hasNext()) { 64 | Host host = Host.fromString(it.nextLine()); 65 | if (host != null) { 66 | mHosts.add(host); 67 | } 68 | } 69 | } catch (IOException e) { 70 | Timber.e(e, "I/O error while opening hosts file"); 71 | } finally { 72 | if (it != null) { 73 | LineIterator.closeQuietly(it); 74 | } 75 | } 76 | } 77 | return mHosts; 78 | } 79 | 80 | /** 81 | * Saves new hosts file and creates a backup of previous file. 82 | *

Must be in an async call.

83 | * 84 | * @param appContext application context 85 | * @return {@code true} if everything was working as expected, or {@code false} otherwise 86 | */ 87 | public synchronized boolean saveHosts(Context appContext) { 88 | if (!RootTools.isAccessGiven()) { 89 | Timber.w("Can't get root access"); 90 | return false; 91 | } 92 | 93 | // Step 1: Create temporary hosts file in /data/data/project_package/files/hosts 94 | if (!createTempHostsFile(appContext)) { 95 | Timber.w("Can't create temporary hosts file"); 96 | return false; 97 | } 98 | 99 | String tmpFile = String.format(Locale.US, "%s/%s", appContext.getFilesDir().getAbsolutePath(), HOSTS_FILE_NAME); 100 | String backupFile = String.format(Locale.US, "%s.bak", tmpFile); 101 | 102 | // Step 2: Get canonical path for /etc/hosts (it could be a symbolic link) 103 | String hostsFilePath = HOSTS_FILE_PATH; 104 | File hostsFile = new File(HOSTS_FILE_PATH); 105 | if (hostsFile.exists()) { 106 | try { 107 | if (FileUtils.isSymlink(hostsFile)) { 108 | hostsFilePath = hostsFile.getCanonicalPath(); 109 | } 110 | } catch (IOException e1) { 111 | Timber.e(e1, "Can't find hosts file"); 112 | } 113 | } else { 114 | Timber.w("Hosts file was not found in filesystem"); 115 | } 116 | 117 | try { 118 | // Step 3: Create backup of current hosts file (if any) 119 | RootTools.remount(hostsFilePath, MOUNT_TYPE_RW); 120 | runRootCommand(COMMAND_RM, backupFile); 121 | RootTools.copyFile(hostsFilePath, backupFile, false, true); 122 | 123 | // Step 4: Replace hosts file with generated file 124 | runRootCommand(COMMAND_RM, hostsFilePath); 125 | RootTools.copyFile(tmpFile, hostsFilePath, false, true); 126 | 127 | // Step 5: Give proper rights 128 | runRootCommand(COMMAND_CHOWN, hostsFilePath); 129 | runRootCommand(COMMAND_CHMOD_644, hostsFilePath); 130 | 131 | // Step 6: Delete local file 132 | appContext.deleteFile(HOSTS_FILE_NAME); 133 | } catch (Exception e) { 134 | Timber.e(e, "Failed running root command"); 135 | return false; 136 | } finally { 137 | RootTools.remount(hostsFilePath, MOUNT_TYPE_RO); 138 | } 139 | return true; 140 | } 141 | 142 | /** 143 | * Returns a list of hosts matching the constraint parameter. 144 | */ 145 | public List filterHosts(CharSequence constraint) { 146 | List all = getHosts(false); 147 | List hosts = new ArrayList<>(); 148 | 149 | for (Host host : all) { 150 | if (host.isValid()) { 151 | if (host.getIp().contains(constraint) 152 | || host.getHostName().contains(constraint) 153 | || (host.getComment() != null && host.getComment().contains(constraint))) { 154 | hosts.add(host); 155 | } 156 | } 157 | } 158 | return hosts; 159 | } 160 | 161 | /** 162 | * Creates a temporary hosts file in {@code /data/data/project_package/files/hosts}. 163 | *

Must be in an async call.

164 | * 165 | * @param appContext application context 166 | * @return {@code true} if the temp file was created, or {@code false} otherwise 167 | */ 168 | private boolean createTempHostsFile(Context appContext) { 169 | OutputStreamWriter writer = null; 170 | try { 171 | FileOutputStream out = appContext.openFileOutput(HOSTS_FILE_NAME, Context.MODE_PRIVATE); 172 | writer = new OutputStreamWriter(out); 173 | 174 | for (Host host : getHosts(false)) { 175 | writer.append(host.toString()).append(LINE_SEPARATOR); 176 | } 177 | writer.flush(); 178 | } catch (IOException e) { 179 | Timber.e(e, "Error creating temporary hosts file"); 180 | return false; 181 | } finally { 182 | if (writer != null) { 183 | try { 184 | writer.close(); 185 | } catch (IOException e) { 186 | Timber.e(e, "Error while closing writer"); 187 | } 188 | } 189 | } 190 | return true; 191 | } 192 | 193 | /** 194 | * Executes a single argument root command. 195 | *

Must be in an async call.

196 | * 197 | * @param command a command, ie {@code "rm -f"}, {@code "chmod 644"}... 198 | * @param uniqueArg the unique argument for the command, usually the file name 199 | */ 200 | private void runRootCommand(String command, String uniqueArg) throws IOException, TimeoutException, RootDeniedException { 201 | Command cmd = new Command(0, false, String.format(Locale.US, "%s %s", command, uniqueArg)); 202 | RootShell.getShell(true).add(cmd); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/ui/list/ListHostsActivity.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.ui.list; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v4.app.FragmentManager; 6 | import android.support.v4.app.FragmentTransaction; 7 | import android.text.TextUtils; 8 | import android.view.KeyEvent; 9 | import android.view.View; 10 | import android.widget.ProgressBar; 11 | import android.widget.TextView; 12 | 13 | import com.actionbarsherlock.view.Menu; 14 | import com.actionbarsherlock.view.MenuItem; 15 | import com.actionbarsherlock.widget.SearchView; 16 | import com.nilhcem.hostseditor.R; 17 | import com.nilhcem.hostseditor.core.Host; 18 | import com.nilhcem.hostseditor.event.LoadingEvent; 19 | import com.nilhcem.hostseditor.event.StartAddEditActivityEvent; 20 | import com.nilhcem.hostseditor.ui.BaseActivity; 21 | import com.nilhcem.hostseditor.ui.about.AboutDialogFragment; 22 | import com.nilhcem.hostseditor.ui.addedit.AddEditHostActivity; 23 | import com.squareup.otto.Subscribe; 24 | 25 | import butterknife.Bind; 26 | import butterknife.ButterKnife; 27 | import timber.log.Timber; 28 | 29 | public class ListHostsActivity extends BaseActivity { 30 | 31 | private static final int REQUESTCODE_ADDEDIT_ACTIVITY = 1; 32 | private static final String STR_EMPTY = ""; 33 | private static final String INSTANCE_STATE_LOADING = "loading"; 34 | private static final String INSTANCE_STATE_LOADING_MESSAGE = "loadingMessage"; 35 | private static final String INSTANCE_STATE_SEARCH = "search"; 36 | 37 | @Bind(R.id.listLoading) ProgressBar mProgressBar; 38 | @Bind(R.id.listLoadingMsg) TextView mLoadingMsg; 39 | 40 | private ListHostsFragment mFragment; 41 | private String mSearchQuery = STR_EMPTY; 42 | private MenuItem mSearchItem; 43 | 44 | @Override 45 | protected void onCreate(Bundle savedInstanceState) { 46 | super.onCreate(savedInstanceState); 47 | setContentView(R.layout.list_hosts_layout); 48 | ButterKnife.bind(this); 49 | 50 | FragmentManager fm = getSupportFragmentManager(); 51 | mFragment = (ListHostsFragment) fm.findFragmentById(R.id.listHostsFragment); 52 | 53 | if (savedInstanceState == null) { 54 | onLoadingEvent(new LoadingEvent(true, R.string.loading_hosts)); 55 | } 56 | } 57 | 58 | @Override 59 | public boolean onCreateOptionsMenu(Menu menu) { 60 | getSupportMenuInflater().inflate(R.menu.list_menu, menu); 61 | 62 | // Init search 63 | mSearchItem = menu.findItem(R.id.action_search); 64 | SearchView searchView = (SearchView) mSearchItem.getActionView(); 65 | if (searchView != null) { 66 | searchView.setQueryHint(getString(R.string.action_search)); 67 | searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { 68 | @Override 69 | public boolean onQueryTextSubmit(String query) { 70 | mSearchItem.collapseActionView(); 71 | return true; 72 | } 73 | 74 | @Override 75 | public boolean onQueryTextChange(String newText) { 76 | Timber.d("Search query: %s", newText); 77 | setSearchQuery(newText); 78 | mFragment.filterList(newText); 79 | return true; 80 | } 81 | }); 82 | } 83 | return true; 84 | } 85 | 86 | @Override 87 | protected void onResume() { 88 | super.onResume(); 89 | mFragment.filterList(mSearchQuery); 90 | } 91 | 92 | @Override 93 | public boolean onKeyDown(int keyCode, KeyEvent event) { 94 | switch (keyCode) { 95 | case KeyEvent.KEYCODE_BACK: 96 | if (!TextUtils.isEmpty(mSearchQuery)) { 97 | // Revert search 98 | setSearchQuery(STR_EMPTY); 99 | mFragment.filterList(mSearchQuery); 100 | return true; 101 | } 102 | break; 103 | case KeyEvent.KEYCODE_SEARCH: 104 | mSearchItem.expandActionView(); 105 | return true; 106 | } 107 | return super.onKeyDown(keyCode, event); 108 | } 109 | 110 | @Override 111 | public boolean onOptionsItemSelected(MenuItem item) { 112 | switch (item.getItemId()) { 113 | case R.id.action_add_host: 114 | mBus.post(new StartAddEditActivityEvent(null)); 115 | return true; 116 | case R.id.action_reload_hosts: 117 | mFragment.refreshHosts(true); 118 | return true; 119 | case R.id.action_select_all: 120 | mFragment.selectAll(); 121 | return true; 122 | case R.id.action_about: 123 | displayAboutDialog(); 124 | return true; 125 | default: 126 | return super.onOptionsItemSelected(item); 127 | } 128 | } 129 | 130 | @Override 131 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 132 | super.onActivityResult(requestCode, resultCode, data); 133 | 134 | Timber.d("Activity result received"); 135 | if (requestCode == REQUESTCODE_ADDEDIT_ACTIVITY) { 136 | if (resultCode == RESULT_OK) { 137 | Host modified = data.getParcelableExtra(AddEditHostActivity.EXTRA_HOST_MODIFIED); 138 | Host original = data.getParcelableExtra(AddEditHostActivity.EXTRA_HOST_ORIGINAL); 139 | 140 | boolean addMode = (original == null); 141 | onLoadingEvent(new LoadingEvent(true, addMode ? R.string.loading_add : R.string.loading_edit)); 142 | mFragment.addEditHost(addMode, new Host[]{modified, original}); 143 | } 144 | } 145 | } 146 | 147 | @Override 148 | protected void onSaveInstanceState(Bundle outState) { 149 | super.onSaveInstanceState(outState); 150 | outState.putBoolean(INSTANCE_STATE_LOADING, mProgressBar.getVisibility() == View.VISIBLE); 151 | outState.putString(INSTANCE_STATE_LOADING_MESSAGE, mLoadingMsg.getText().toString()); 152 | outState.putString(INSTANCE_STATE_SEARCH, mSearchQuery); 153 | } 154 | 155 | @Override 156 | protected void onRestoreInstanceState(Bundle savedInstanceState) { 157 | super.onRestoreInstanceState(savedInstanceState); 158 | boolean isLoading = savedInstanceState.getBoolean(INSTANCE_STATE_LOADING, true); 159 | 160 | if (isLoading) { 161 | String loadingMsg = savedInstanceState.getString(INSTANCE_STATE_LOADING_MESSAGE); 162 | if (loadingMsg == null) { 163 | loadingMsg = STR_EMPTY; 164 | } 165 | onLoadingEvent(new LoadingEvent(true, loadingMsg)); 166 | } 167 | setSearchQuery(savedInstanceState.getString(INSTANCE_STATE_SEARCH)); 168 | } 169 | 170 | @Override 171 | protected void onDestroy() { 172 | ButterKnife.unbind(this); 173 | super.onDestroy(); 174 | } 175 | 176 | @Subscribe 177 | public void onStartAddEditActivityEvent(StartAddEditActivityEvent event) { 178 | Timber.d("Ready to start AddEditActivity"); 179 | Intent intent = new Intent(this, AddEditHostActivity.class); 180 | intent.putExtra(AddEditHostActivity.EXTRA_HOST_ORIGINAL, event.host); 181 | startActivityForResult(intent, REQUESTCODE_ADDEDIT_ACTIVITY); 182 | } 183 | 184 | @Subscribe 185 | public void onLoadingEvent(LoadingEvent event) { 186 | FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); 187 | 188 | mLoadingMsg.setText(event.getMessage(this)); 189 | if (event.isLoading()) { 190 | Timber.d("Start loading"); 191 | if (mSearchItem != null) { 192 | setSearchQuery(STR_EMPTY); 193 | mSearchItem.collapseActionView(); 194 | } 195 | 196 | mProgressBar.setVisibility(View.VISIBLE); 197 | mLoadingMsg.setVisibility(View.VISIBLE); 198 | ft.hide(mFragment); 199 | } else { 200 | Timber.d("Stop loading"); 201 | mProgressBar.setVisibility(View.GONE); 202 | mLoadingMsg.setVisibility(View.GONE); 203 | ft.show(mFragment); 204 | } 205 | ft.commitAllowingStateLoss(); 206 | } 207 | 208 | private void setSearchQuery(String searchQuery) { 209 | mSearchQuery = searchQuery; 210 | setActionBarTitle(); 211 | } 212 | 213 | private void setActionBarTitle() { 214 | int titleRes; 215 | 216 | if (TextUtils.isEmpty(mSearchQuery)) { 217 | titleRes = R.string.app_name; 218 | } else { 219 | titleRes = R.string.action_search_results; 220 | } 221 | getSupportActionBar().setTitle(titleRes); 222 | } 223 | 224 | private void displayAboutDialog() { 225 | FragmentManager fm = getSupportFragmentManager(); 226 | AboutDialogFragment dialog = new AboutDialogFragment(); 227 | dialog.show(fm, AboutDialogFragment.TAG); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /app/src/main/java/com/nilhcem/hostseditor/ui/list/ListHostsFragment.java: -------------------------------------------------------------------------------- 1 | package com.nilhcem.hostseditor.ui.list; 2 | 3 | import android.app.AlertDialog; 4 | import android.app.Dialog; 5 | import android.content.DialogInterface; 6 | import android.os.Bundle; 7 | import android.util.SparseBooleanArray; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.AdapterView; 12 | import android.widget.AdapterView.OnItemClickListener; 13 | import android.widget.AdapterView.OnItemLongClickListener; 14 | import android.widget.ListView; 15 | 16 | import com.actionbarsherlock.view.ActionMode; 17 | import com.actionbarsherlock.view.Menu; 18 | import com.actionbarsherlock.view.MenuInflater; 19 | import com.actionbarsherlock.view.MenuItem; 20 | import com.nilhcem.hostseditor.R; 21 | import com.nilhcem.hostseditor.core.Host; 22 | import com.nilhcem.hostseditor.event.LoadingEvent; 23 | import com.nilhcem.hostseditor.event.RefreshHostsEvent; 24 | import com.nilhcem.hostseditor.event.StartAddEditActivityEvent; 25 | import com.nilhcem.hostseditor.event.TaskCompletedEvent; 26 | import com.nilhcem.hostseditor.task.AddEditHostAsync; 27 | import com.nilhcem.hostseditor.task.ListHostsAsync; 28 | import com.nilhcem.hostseditor.task.RemoveHostsAsync; 29 | import com.nilhcem.hostseditor.task.ToggleHostsAsync; 30 | import com.nilhcem.hostseditor.ui.BaseFragment; 31 | import com.squareup.otto.Subscribe; 32 | 33 | import java.util.ArrayList; 34 | import java.util.List; 35 | import java.util.Locale; 36 | 37 | import javax.inject.Inject; 38 | 39 | import butterknife.Bind; 40 | import butterknife.ButterKnife; 41 | import timber.log.Timber; 42 | 43 | public class ListHostsFragment extends BaseFragment implements OnItemClickListener, OnItemLongClickListener { 44 | 45 | @Inject ListHostsAdapter mAdapter; 46 | 47 | @Bind(R.id.listHosts) ListView mListView; 48 | 49 | private ActionMode mMode; 50 | private MenuItem mEditMenuItem; 51 | private Dialog mDisplayedDialog; 52 | 53 | @Override 54 | public void onCreate(Bundle savedInstanceState) { 55 | super.onCreate(savedInstanceState); 56 | mApp.component().inject(this); 57 | mAdapter.init(mApp); 58 | } 59 | 60 | @Override 61 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 62 | boolean firstCall = (mListView == null); 63 | 64 | View view = inflater.inflate(R.layout.list_hosts_fragment, container, false); 65 | ButterKnife.bind(this, view); 66 | 67 | mListView.setAdapter(mAdapter); 68 | mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); 69 | mListView.setFastScrollEnabled(true); 70 | mListView.setItemsCanFocus(false); 71 | mListView.setOnItemClickListener(this); 72 | mListView.setOnItemLongClickListener(this); 73 | 74 | View emptyLayout = view.findViewById(R.id.listEmptyLayout); 75 | if (emptyLayout != null) { 76 | mListView.setEmptyView(emptyLayout); 77 | } 78 | 79 | mAdapter.computeViewWidths(mActivity); 80 | 81 | if (firstCall) { 82 | refreshHosts(false); 83 | } 84 | return view; 85 | } 86 | 87 | @Override 88 | public void onPause() { 89 | finishActionMode(); 90 | if (mDisplayedDialog != null) { 91 | mDisplayedDialog.dismiss(); 92 | mDisplayedDialog = null; 93 | } 94 | super.onPause(); 95 | } 96 | 97 | @Override 98 | public void onDestroyView() { 99 | ButterKnife.unbind(this); 100 | super.onDestroyView(); 101 | } 102 | 103 | @Override 104 | public void onItemClick(AdapterView parent, View view, int position, long id) { 105 | int nbCheckedElements = 0; 106 | SparseBooleanArray checked = mListView.getCheckedItemPositions(); 107 | for (int i = 0; i < checked.size(); i++) { 108 | if (checked.valueAt(i)) { 109 | nbCheckedElements++; 110 | } 111 | } 112 | displayActionMode(nbCheckedElements); 113 | } 114 | 115 | @Override 116 | public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { 117 | if (mMode == null) { 118 | Host host = mAdapter.getItem(position); 119 | mBus.post(new StartAddEditActivityEvent(host)); 120 | return true; 121 | } 122 | return false; 123 | } 124 | 125 | @Subscribe 126 | public void onTaskFinished(TaskCompletedEvent task) { 127 | Timber.d("Task %s finished", task.tag); 128 | if (task.isSuccessful) { 129 | refreshHosts(false); 130 | } else { 131 | displayErrorDialog(); 132 | } 133 | } 134 | 135 | @Subscribe 136 | public void onHostsRefreshed(RefreshHostsEvent hosts) { 137 | Timber.d("Refreshing listview"); 138 | finishActionMode(); 139 | mAdapter.updateHosts(hosts.hosts); 140 | mBus.post(new LoadingEvent()); 141 | } 142 | 143 | public void addEditHost(boolean addMode, Host[] hosts) { 144 | new AddEditHostAsync(mApp, addMode).execute(hosts); 145 | } 146 | 147 | public void refreshHosts(boolean forceRefresh) { 148 | new ListHostsAsync(mApp).execute(forceRefresh); 149 | } 150 | 151 | public void selectAll() { 152 | int nbElem = mAdapter.getCount(); 153 | 154 | if (nbElem > 0) { 155 | for (int i = 0; i < nbElem; i++) { 156 | mListView.setItemChecked(i, true); 157 | } 158 | displayActionMode(nbElem); 159 | } 160 | } 161 | 162 | public void filterList(String filter) { 163 | mAdapter.getFilter().filter(filter); 164 | } 165 | 166 | private void displayActionMode(int nbCheckedElements) { 167 | if (nbCheckedElements > 0) { 168 | if (mMode == null) { 169 | mMode = mActivity.startActionMode(new ModeCallback()); 170 | } 171 | 172 | if (mEditMenuItem != null) { 173 | mEditMenuItem.setVisible(nbCheckedElements == 1); 174 | } 175 | mMode.setTitle(String.format(Locale.US, getString(R.string.list_menu_selected), nbCheckedElements)); 176 | } else { 177 | finishActionMode(); 178 | } 179 | } 180 | 181 | private final class ModeCallback implements ActionMode.Callback { 182 | 183 | @Override 184 | public boolean onCreateActionMode(ActionMode mode, Menu menu) { 185 | MenuInflater inflater = mActivity.getSupportMenuInflater(); 186 | inflater.inflate(R.menu.list_contextual_actions, menu); 187 | return true; 188 | } 189 | 190 | @Override 191 | public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 192 | mEditMenuItem = menu.findItem(R.id.cab_action_edit); 193 | return false; 194 | } 195 | 196 | @Override 197 | public void onDestroyActionMode(ActionMode mode) { 198 | for (int i = 0; i < mListView.getAdapter().getCount(); i++) { 199 | mListView.setItemChecked(i, false); 200 | } 201 | mMode = null; 202 | mEditMenuItem = null; 203 | } 204 | 205 | @Override 206 | public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 207 | Host[] selectedItems = getSelectedItems(); 208 | 209 | switch (item.getItemId()) { 210 | case R.id.cab_action_edit: 211 | mBus.post(new StartAddEditActivityEvent(selectedItems[0])); 212 | break; 213 | case R.id.cab_action_delete: 214 | displayDeleteConfirmationDialog(selectedItems); 215 | break; 216 | case R.id.cab_action_toggle: 217 | new ToggleHostsAsync(mApp, selectedItems.length == 1).execute(selectedItems); 218 | break; 219 | default: 220 | return false; 221 | } 222 | mode.finish(); 223 | return true; 224 | } 225 | 226 | private Host[] getSelectedItems() { 227 | List items = new ArrayList<>(); 228 | 229 | int len = mListView.getCount(); 230 | SparseBooleanArray checked = mListView.getCheckedItemPositions(); 231 | for (int i = 0; i < len; i++) { 232 | if (checked.get(i)) { 233 | items.add(mAdapter.getItem(i)); 234 | } 235 | } 236 | return items.toArray(new Host[items.size()]); 237 | } 238 | } 239 | 240 | private void displayDeleteConfirmationDialog(final Host[] selectedItems) { 241 | mDisplayedDialog = new AlertDialog.Builder(mActivity) 242 | .setTitle(R.string.delete_dialog_title) 243 | .setMessage(getResources().getQuantityText(R.plurals.delete_dialog_content, selectedItems.length)) 244 | .setPositiveButton(R.string.delete_dialog_delete, new DialogInterface.OnClickListener() { 245 | @Override 246 | public void onClick(DialogInterface dialog, int which) { 247 | new RemoveHostsAsync(mApp, selectedItems.length == 1).execute(selectedItems); 248 | } 249 | }) 250 | .setNegativeButton(R.string.delete_dialog_cancel, new DialogInterface.OnClickListener() { 251 | @Override 252 | public void onClick(DialogInterface dialog, int which) { 253 | // Do nothing 254 | } 255 | }) 256 | .create(); 257 | mDisplayedDialog.show(); 258 | } 259 | 260 | private void displayErrorDialog() { 261 | mBus.post(new LoadingEvent()); 262 | mDisplayedDialog = new AlertDialog.Builder(mActivity) 263 | .setTitle(R.string.list_error_title) 264 | .setMessage(R.string.list_error_content) 265 | .setNeutralButton(R.string.list_error_ok, new DialogInterface.OnClickListener() { 266 | @Override 267 | public void onClick(DialogInterface dialog, int which) { 268 | refreshHosts(true); 269 | } 270 | }) 271 | .setOnCancelListener(new DialogInterface.OnCancelListener() { 272 | @Override 273 | public void onCancel(DialogInterface dialog) { 274 | refreshHosts(true); 275 | } 276 | }) 277 | .create(); 278 | mDisplayedDialog.show(); 279 | } 280 | 281 | private void finishActionMode() { 282 | if (mMode != null) { 283 | mMode.finish(); 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hosts Editor for Android 2 | 3 | This repository contains the source code for the Hosts-editor Android app. 4 | 5 | It is available from [Google Play][] and [F-Droid][]. 6 | 7 | ![Hosts Editor for Android screenshots][] 8 | 9 | This application, **for root devices only**, lets you modify easily your Android `/etc/hosts` file. 10 | 11 | You might need to reboot to clear your DNS cache after any change. 12 | 13 | 14 | ## Features 15 | 16 | * Android 2.2 and above 17 | * Insert, Remove, Toggle (comment/uncomment) and Modify host entries 18 | * Holo theme 19 | * Fast search engine 20 | * 100% free/libre, gratis, no ads 21 | 22 | 23 | ## Building the app 24 | 25 | The Android SDK for API level 19 is required to build the app. Then launch: 26 | 27 | ./gradlew clean build 28 | 29 | 30 | ## Acknowledgements 31 | 32 | This project uses many great open-source libraries, such as: 33 | 34 | * [ActionBarSherlock][] 35 | * [Butterknife][] 36 | * [Dagger][] 37 | * [Commons IO][] 38 | * [Otto][] 39 | * [RootTools][] 40 | 41 | 42 | ## License 43 | 44 |
 45 | 
 46 |                                  Apache License
 47 |                            Version 2.0, January 2004
 48 |                         http://www.apache.org/licenses/
 49 | 
 50 |    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
 51 | 
 52 |    1. Definitions.
 53 | 
 54 |       "License" shall mean the terms and conditions for use, reproduction,
 55 |       and distribution as defined by Sections 1 through 9 of this document.
 56 | 
 57 |       "Licensor" shall mean the copyright owner or entity authorized by
 58 |       the copyright owner that is granting the License.
 59 | 
 60 |       "Legal Entity" shall mean the union of the acting entity and all
 61 |       other entities that control, are controlled by, or are under common
 62 |       control with that entity. For the purposes of this definition,
 63 |       "control" means (i) the power, direct or indirect, to cause the
 64 |       direction or management of such entity, whether by contract or
 65 |       otherwise, or (ii) ownership of fifty percent (50%) or more of the
 66 |       outstanding shares, or (iii) beneficial ownership of such entity.
 67 | 
 68 |       "You" (or "Your") shall mean an individual or Legal Entity
 69 |       exercising permissions granted by this License.
 70 | 
 71 |       "Source" form shall mean the preferred form for making modifications,
 72 |       including but not limited to software source code, documentation
 73 |       source, and configuration files.
 74 | 
 75 |       "Object" form shall mean any form resulting from mechanical
 76 |       transformation or translation of a Source form, including but
 77 |       not limited to compiled object code, generated documentation,
 78 |       and conversions to other media types.
 79 | 
 80 |       "Work" shall mean the work of authorship, whether in Source or
 81 |       Object form, made available under the License, as indicated by a
 82 |       copyright notice that is included in or attached to the work
 83 |       (an example is provided in the Appendix below).
 84 | 
 85 |       "Derivative Works" shall mean any work, whether in Source or Object
 86 |       form, that is based on (or derived from) the Work and for which the
 87 |       editorial revisions, annotations, elaborations, or other modifications
 88 |       represent, as a whole, an original work of authorship. For the purposes
 89 |       of this License, Derivative Works shall not include works that remain
 90 |       separable from, or merely link (or bind by name) to the interfaces of,
 91 |       the Work and Derivative Works thereof.
 92 | 
 93 |       "Contribution" shall mean any work of authorship, including
 94 |       the original version of the Work and any modifications or additions
 95 |       to that Work or Derivative Works thereof, that is intentionally
 96 |       submitted to Licensor for inclusion in the Work by the copyright owner
 97 |       or by an individual or Legal Entity authorized to submit on behalf of
 98 |       the copyright owner. For the purposes of this definition, "submitted"
 99 |       means any form of electronic, verbal, or written communication sent
100 |       to the Licensor or its representatives, including but not limited to
101 |       communication on electronic mailing lists, source code control systems,
102 |       and issue tracking systems that are managed by, or on behalf of, the
103 |       Licensor for the purpose of discussing and improving the Work, but
104 |       excluding communication that is conspicuously marked or otherwise
105 |       designated in writing by the copyright owner as "Not a Contribution."
106 | 
107 |       "Contributor" shall mean Licensor and any individual or Legal Entity
108 |       on behalf of whom a Contribution has been received by Licensor and
109 |       subsequently incorporated within the Work.
110 | 
111 |    2. Grant of Copyright License. Subject to the terms and conditions of
112 |       this License, each Contributor hereby grants to You a perpetual,
113 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
114 |       copyright license to reproduce, prepare Derivative Works of,
115 |       publicly display, publicly perform, sublicense, and distribute the
116 |       Work and such Derivative Works in Source or Object form.
117 | 
118 |    3. Grant of Patent License. Subject to the terms and conditions of
119 |       this License, each Contributor hereby grants to You a perpetual,
120 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
121 |       (except as stated in this section) patent license to make, have made,
122 |       use, offer to sell, sell, import, and otherwise transfer the Work,
123 |       where such license applies only to those patent claims licensable
124 |       by such Contributor that are necessarily infringed by their
125 |       Contribution(s) alone or by combination of their Contribution(s)
126 |       with the Work to which such Contribution(s) was submitted. If You
127 |       institute patent litigation against any entity (including a
128 |       cross-claim or counterclaim in a lawsuit) alleging that the Work
129 |       or a Contribution incorporated within the Work constitutes direct
130 |       or contributory patent infringement, then any patent licenses
131 |       granted to You under this License for that Work shall terminate
132 |       as of the date such litigation is filed.
133 | 
134 |    4. Redistribution. You may reproduce and distribute copies of the
135 |       Work or Derivative Works thereof in any medium, with or without
136 |       modifications, and in Source or Object form, provided that You
137 |       meet the following conditions:
138 | 
139 |       (a) You must give any other recipients of the Work or
140 |           Derivative Works a copy of this License; and
141 | 
142 |       (b) You must cause any modified files to carry prominent notices
143 |           stating that You changed the files; and
144 | 
145 |       (c) You must retain, in the Source form of any Derivative Works
146 |           that You distribute, all copyright, patent, trademark, and
147 |           attribution notices from the Source form of the Work,
148 |           excluding those notices that do not pertain to any part of
149 |           the Derivative Works; and
150 | 
151 |       (d) If the Work includes a "NOTICE" text file as part of its
152 |           distribution, then any Derivative Works that You distribute must
153 |           include a readable copy of the attribution notices contained
154 |           within such NOTICE file, excluding those notices that do not
155 |           pertain to any part of the Derivative Works, in at least one
156 |           of the following places: within a NOTICE text file distributed
157 |           as part of the Derivative Works; within the Source form or
158 |           documentation, if provided along with the Derivative Works; or,
159 |           within a display generated by the Derivative Works, if and
160 |           wherever such third-party notices normally appear. The contents
161 |           of the NOTICE file are for informational purposes only and
162 |           do not modify the License. You may add Your own attribution
163 |           notices within Derivative Works that You distribute, alongside
164 |           or as an addendum to the NOTICE text from the Work, provided
165 |           that such additional attribution notices cannot be construed
166 |           as modifying the License.
167 | 
168 |       You may add Your own copyright statement to Your modifications and
169 |       may provide additional or different license terms and conditions
170 |       for use, reproduction, or distribution of Your modifications, or
171 |       for any such Derivative Works as a whole, provided Your use,
172 |       reproduction, and distribution of the Work otherwise complies with
173 |       the conditions stated in this License.
174 | 
175 |    5. Submission of Contributions. Unless You explicitly state otherwise,
176 |       any Contribution intentionally submitted for inclusion in the Work
177 |       by You to the Licensor shall be under the terms and conditions of
178 |       this License, without any additional terms or conditions.
179 |       Notwithstanding the above, nothing herein shall supersede or modify
180 |       the terms of any separate license agreement you may have executed
181 |       with Licensor regarding such Contributions.
182 | 
183 |    6. Trademarks. This License does not grant permission to use the trade
184 |       names, trademarks, service marks, or product names of the Licensor,
185 |       except as required for reasonable and customary use in describing the
186 |       origin of the Work and reproducing the content of the NOTICE file.
187 | 
188 |    7. Disclaimer of Warranty. Unless required by applicable law or
189 |       agreed to in writing, Licensor provides the Work (and each
190 |       Contributor provides its Contributions) on an "AS IS" BASIS,
191 |       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
192 |       implied, including, without limitation, any warranties or conditions
193 |       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
194 |       PARTICULAR PURPOSE. You are solely responsible for determining the
195 |       appropriateness of using or redistributing the Work and assume any
196 |       risks associated with Your exercise of permissions under this License.
197 | 
198 |    8. Limitation of Liability. In no event and under no legal theory,
199 |       whether in tort (including negligence), contract, or otherwise,
200 |       unless required by applicable law (such as deliberate and grossly
201 |       negligent acts) or agreed to in writing, shall any Contributor be
202 |       liable to You for damages, including any direct, indirect, special,
203 |       incidental, or consequential damages of any character arising as a
204 |       result of this License or out of the use or inability to use the
205 |       Work (including but not limited to damages for loss of goodwill,
206 |       work stoppage, computer failure or malfunction, or any and all
207 |       other commercial damages or losses), even if such Contributor
208 |       has been advised of the possibility of such damages.
209 | 
210 |    9. Accepting Warranty or Additional Liability. While redistributing
211 |       the Work or Derivative Works thereof, You may choose to offer,
212 |       and charge a fee for, acceptance of support, warranty, indemnity,
213 |       or other liability obligations and/or rights consistent with this
214 |       License. However, in accepting such obligations, You may act only
215 |       on Your own behalf and on Your sole responsibility, not on behalf
216 |       of any other Contributor, and only if You agree to indemnify,
217 |       defend, and hold each Contributor harmless for any liability
218 |       incurred by, or claims asserted against, such Contributor by reason
219 |       of your accepting any such warranty or additional liability.
220 | 
221 |    END OF TERMS AND CONDITIONS
222 | 
223 |    APPENDIX: How to apply the Apache License to your work.
224 | 
225 |       To apply the Apache License to your work, attach the following
226 |       boilerplate notice, with the fields enclosed by brackets "[]"
227 |       replaced with your own identifying information. (Don't include
228 |       the brackets!)  The text should be enclosed in the appropriate
229 |       comment syntax for the file format. We also recommend that a
230 |       file or class name and description of purpose be included on the
231 |       same "printed page" as the copyright notice for easier
232 |       identification within third-party archives.
233 | 
234 |    Copyright [yyyy] [name of copyright owner]
235 | 
236 |    Licensed under the Apache License, Version 2.0 (the "License");
237 |    you may not use this file except in compliance with the License.
238 |    You may obtain a copy of the License at
239 | 
240 |        http://www.apache.org/licenses/LICENSE-2.0
241 | 
242 |    Unless required by applicable law or agreed to in writing, software
243 |    distributed under the License is distributed on an "AS IS" BASIS,
244 |    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
245 |    See the License for the specific language governing permissions and
246 |    limitations under the License.
247 | 
248 | 249 | 250 | [Google Play]: https://play.google.com/store/apps/details?id=com.nilhcem.hostseditor 251 | [F-Droid]: https://f-droid.org/repository/browse/?fdid=com.nilhcem.hostseditor 252 | [Hosts Editor for Android screenshots]: https://raw.githubusercontent.com/Nilhcem/hosts-editor-android/master/screenshot.png 253 | [ActionBarSherlock]: http://actionbarsherlock.com/ 254 | [Butterknife]: http://jakewharton.github.com/butterknife/ 255 | [Dagger]: http://square.github.com/dagger/ 256 | [Commons IO]: http://commons.apache.org/proper/commons-io/ 257 | [Otto]: http://square.github.com/otto/ 258 | [RootTools]: https://code.google.com/p/roottools/ 259 | --------------------------------------------------------------------------------