├── 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 |
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 ListMust 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 ListMust 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 | List45 | 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 | --------------------------------------------------------------------------------