diff --git a/app/build.gradle b/app/build.gradle index b9631646..cc364c78 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -30,5 +30,5 @@ dependencies { compile project(':orbotservice') compile 'com.android.support:support-v4:23.4.0' compile 'com.android.support:appcompat-v7:23.4.0' - + compile 'com.android.support:design:23.4.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 171567ec..a713cf56 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,137 +1,178 @@ - - - - - - - + --> - + + + + + + - > - - + + + + android:launchMode="singleTop"> + - - + + + - - + + - + + - - - - - - - - - - - - - + + + + + + + + android:stopWithTask="false"> + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/torproject/android/OrbotMainActivity.java b/app/src/main/java/org/torproject/android/OrbotMainActivity.java index 7706c0f5..1a468d4c 100644 --- a/app/src/main/java/org/torproject/android/OrbotMainActivity.java +++ b/app/src/main/java/org/torproject/android/OrbotMainActivity.java @@ -4,6 +4,7 @@ package org.torproject.android; import java.io.BufferedReader; +import java.io.File; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; @@ -26,6 +27,11 @@ import org.torproject.android.ui.AppManager; import org.torproject.android.ui.ImageProgressView; import org.torproject.android.ui.PromoAppsActivity; import org.torproject.android.ui.Rotate3dAnimation; +import org.torproject.android.ui.hiddenservices.ClientCookiesActivity; +import org.torproject.android.ui.hiddenservices.HiddenServicesActivity; +import org.torproject.android.ui.hiddenservices.backup.BackupUtils; +import org.torproject.android.ui.hiddenservices.permissions.PermissionManager; +import org.torproject.android.ui.hiddenservices.providers.HSContentProvider; import org.torproject.android.vpn.VPNEnableActivity; import android.annotation.SuppressLint; @@ -34,6 +40,8 @@ import android.app.ActivityManager.RunningServiceInfo; import android.app.AlertDialog; import android.app.Dialog; import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -43,6 +51,7 @@ import android.content.SharedPreferences.Editor; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -80,6 +89,8 @@ import android.widget.Toast; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; +import static android.support.v4.content.FileProvider.getUriForFile; + public class OrbotMainActivity extends AppCompatActivity implements OrbotConstants, OnLongClickListener, OnTouchListener { @@ -114,8 +125,6 @@ public class OrbotMainActivity extends AppCompatActivity private final static int REQUEST_SETTINGS = 0x9874; private final static int REQUEST_VPN_APPS_SELECT = 8889; - private final static boolean mIsLollipop = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - // message types for mStatusUpdateHandler private final static int STATUS_UPDATE = 1; private static final int MESSAGE_TRAFFIC_COUNT = 2; @@ -140,13 +149,37 @@ public class OrbotMainActivity extends AppCompatActivity return super.onCreateView(parent, name, context, attrs); return null; } - - /** Called when the activity is first created. */ + + private void migratePreferences() { + String hsPortString = mPrefs.getString("pref_hs_ports", ""); + if (hsPortString.length() > 0) { + StringTokenizer st = new StringTokenizer(hsPortString, ","); + ContentResolver cr = getContentResolver(); + while (st.hasMoreTokens()) { + int hsPort = Integer.parseInt(st.nextToken().split(" ")[0]); + ContentValues fields = new ContentValues(); + fields.put("name", hsPort); + fields.put("port", hsPort); + fields.put("onion_port", hsPort); + cr.insert(HSContentProvider.CONTENT_URI, fields); + } + + Editor pEdit = mPrefs.edit(); + pEdit.remove("pref_hs_ports"); + pEdit.commit(); + } + } + + /** + * Called when the activity is first created. + */ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPrefs = TorServiceUtils.getSharedPrefs(getApplicationContext()); + migratePreferences(); // Migrate old preferences + /* Create the widgets before registering for broadcasts to guarantee * that the widgets exist when the status updates try to update them */ doLayout(); @@ -478,6 +511,10 @@ public class OrbotMainActivity extends AppCompatActivity } } + } else if (item.getItemId() == R.id.menu_hidden_services) { + startActivity(new Intent(this, HiddenServicesActivity.class)); + } else if (item.getItemId() == R.id.menu_client_cookies) { + startActivity(new Intent(this, ClientCookiesActivity.class)); } return super.onOptionsItemSelected(item); @@ -548,192 +585,249 @@ public class OrbotMainActivity extends AppCompatActivity Prefs.putUseVpn(enable); if (enable) { - if (mIsLollipop) //let the user choose the apps + if (PermissionManager.isLollipopOrHigher()) //let the user choose the apps startActivityForResult(new Intent(OrbotMainActivity.this, AppManager.class), REQUEST_VPN_APPS_SELECT); else startActivity(new Intent(OrbotMainActivity.this, VPNEnableActivity.class)); } else stopVpnService(); } - - private void enableHiddenServicePort (int hsPort) throws RemoteException, InterruptedException - { - - Editor pEdit = mPrefs.edit(); - - String hsPortString = mPrefs.getString("pref_hs_ports", ""); - String onionHostname = mPrefs.getString("pref_hs_hostname",""); - - if (hsPortString.indexOf(hsPort+"")==-1) - { - if (hsPortString.length() > 0 && hsPortString.indexOf(hsPort+"")==-1) - hsPortString += ',' + hsPort; - else - hsPortString = hsPort + ""; - - pEdit.putString("pref_hs_ports", hsPortString); - pEdit.putBoolean("pref_hs_enable", true); - - pEdit.commit(); - } - if (onionHostname == null || onionHostname.length() == 0) - { - requestTorRereadConfig(); + private void enableHiddenServicePort( + String hsName, final int hsPort, int hsRemotePort, + final String backupToPackage, final Uri hsKeyPath, + final Boolean authCookie + ) throws RemoteException, InterruptedException { - new Thread () { + String onionHostname = null; - public void run () - { - String onionHostname = mPrefs.getString("pref_hs_hostname",""); + if (hsName == null) + hsName = "hs" + hsPort; - while (onionHostname.length() == 0) - { - //we need to stop and start Tor - try { - Thread.sleep(3000); //wait three seconds - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - onionHostname = mPrefs.getString("pref_hs_hostname",""); - } - - Intent nResult = new Intent(); - nResult.putExtra("hs_host", onionHostname); - setResult(RESULT_OK, nResult); - finish(); - } - }.start(); - - } - else - { - Intent nResult = new Intent(); - nResult.putExtra("hs_host", onionHostname); - setResult(RESULT_OK, nResult); - finish(); - } - - } - - private synchronized void handleIntents () - { - if (getIntent() == null) - return; - - // Get intent, action and MIME type - Intent intent = getIntent(); - String action = intent.getAction(); - Log.d(TAG, "handleIntents " + action); + if (hsRemotePort == -1) + hsRemotePort = hsPort; - //String type = intent.getType(); - - if (action == null) - return; - - if (action.equals(INTENT_ACTION_REQUEST_HIDDEN_SERVICE)) - { - final int hiddenServicePortRequest = getIntent().getIntExtra("hs_port", -1); + ContentValues fields = new ContentValues(); + fields.put(HSContentProvider.HiddenService.NAME, hsName); + fields.put(HSContentProvider.HiddenService.PORT, hsPort); + fields.put(HSContentProvider.HiddenService.ONION_PORT, hsRemotePort); + fields.put(HSContentProvider.HiddenService.AUTH_COOKIE, authCookie); - DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { - - public void onClick(DialogInterface dialog, int which) { - switch (which){ - case DialogInterface.BUTTON_POSITIVE: - - try { - enableHiddenServicePort (hiddenServicePortRequest); - - } catch (RemoteException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } + ContentResolver cr = getContentResolver(); - - break; + Cursor row = cr.query( + HSContentProvider.CONTENT_URI, + HSContentProvider.PROJECTION, + HSContentProvider.HiddenService.ONION_PORT + "=" + hsPort, + null, + null + ); - case DialogInterface.BUTTON_NEGATIVE: - //No button clicked - finish(); - break; - } - } - }; + if (row == null || row.getCount() < 1) { + cr.insert(HSContentProvider.CONTENT_URI, fields); + } else { + onionHostname = row.getString(row.getColumnIndex(HSContentProvider.HiddenService.DOMAIN)); + row.close(); + } - String requestMsg = getString(R.string.hidden_service_request, hiddenServicePortRequest); - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(requestMsg).setPositiveButton("Allow", dialogClickListener) - .setNegativeButton("Deny", dialogClickListener).show(); - - return; //don't null the setIntent() as we need it later - } - else if (action.equals(INTENT_ACTION_REQUEST_START_TOR)) - { - autoStartFromIntent = true; - - startTor(); + if (onionHostname == null || onionHostname.length() < 1) { - //never allow backgrounds start from this type of intent start - //app devs who want background starts, can use the service intents - /** - if (Prefs.allowBackgroundStarts()) - { - Intent resultIntent; - if (lastStatusIntent == null) { - resultIntent = new Intent(intent); - } else { - resultIntent = lastStatusIntent; - } - resultIntent.putExtra(TorServiceConstants.EXTRA_STATUS, torStatus); - setResult(RESULT_OK, resultIntent); - finish(); - }*/ - - } - else if (action.equals(Intent.ACTION_VIEW)) - { - String urlString = intent.getDataString(); - - if (urlString != null) - { - - if (urlString.toLowerCase().startsWith("bridge://")) + if (hsKeyPath != null) { + BackupUtils hsutils = new BackupUtils(getApplicationContext()); + hsutils.restoreKeyBackup(hsPort, hsKeyPath); + } - { - String newBridgeValue = urlString.substring(9); //remove the bridge protocol piece - newBridgeValue = URLDecoder.decode(newBridgeValue); //decode the value here + if (torStatus.equals(TorServiceConstants.STATUS_OFF)) { + startTor(); + } else { + stopTor(); + Toast.makeText( + this, R.string.start_tor_again_for_finish_the_process, Toast.LENGTH_LONG + ).show(); + } - showAlert(getString(R.string.bridges_updated),getString(R.string.restart_orbot_to_use_this_bridge_) + newBridgeValue,false); - - setNewBridges(newBridgeValue); - } - } - } - - updateStatus(null); - - setIntent(null); - - - } - - private void setNewBridges (String newBridgeValue) - { + new Thread() { - Prefs.setBridgesList(newBridgeValue); //set the string to a preference - Prefs.putBridgesEnabled(true); - - setResult(RESULT_OK); - - mBtnBridges.setChecked(true); - - enableBridges(true); - } + public void run() { + String hostname = null; + Intent nResult = new Intent(); + + while (hostname == null) { + try { + Thread.sleep(3000); //wait three seconds + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + Cursor onion = getContentResolver().query( + HSContentProvider.CONTENT_URI, + HSContentProvider.PROJECTION, + HSContentProvider.HiddenService.ONION_PORT + "=" + hsPort, + null, + null + ); + + if (onion != null && onion.getCount() > 0) { + onion.moveToNext(); + hostname = onion.getString(onion.getColumnIndex(HSContentProvider.HiddenService.DOMAIN)); + + if(hostname == null || hostname.length() < 1) + continue; + + nResult.putExtra("hs_host", hostname); + + if (authCookie) { + nResult.putExtra( + "hs_auth_cookie", + onion.getString(onion.getColumnIndex(HSContentProvider.HiddenService.AUTH_COOKIE_VALUE)) + ); + } + + if (backupToPackage != null && backupToPackage.length() > 0) { + String servicePath = getFilesDir() + "/" + TorServiceConstants.HIDDEN_SERVICES_DIR + "/hs" + hsPort; + File hidden_service_key = new File(servicePath, "private_key"); + Context context = getApplicationContext(); + + Uri contentUri = getUriForFile( + context, + "org.torproject.android.ui.hiddenservices.storage", + hidden_service_key + ); + + context.grantUriPermission(backupToPackage, contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + nResult.setData(contentUri); + nResult.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + + onion.close(); + setResult(RESULT_OK, nResult); + finish(); + } + } + } + }.start(); + + } else { + Intent nResult = new Intent(); + nResult.putExtra("hs_host", onionHostname); + setResult(RESULT_OK, nResult); + finish(); + } + } + + private synchronized void handleIntents() { + if (getIntent() == null) + return; + + // Get intent, action and MIME type + Intent intent = getIntent(); + String action = intent.getAction(); + Log.d(TAG, "handleIntents " + action); + + //String type = intent.getType(); + + if (action == null) + return; + + switch (action) { + case INTENT_ACTION_REQUEST_HIDDEN_SERVICE: + final int hiddenServicePort = intent.getIntExtra("hs_port", -1); + final int hiddenServiceRemotePort = intent.getIntExtra("hs_onion_port", -1); + final String hiddenServiceName = intent.getStringExtra("hs_name"); + final String backupToPackage = intent.getStringExtra("hs_backup_to_package"); + final Boolean authCookie = intent.getBooleanExtra("hs_auth_cookie", false); + final Uri mKeyUri = intent.getData(); + + DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + try { + enableHiddenServicePort( + hiddenServiceName, hiddenServicePort, + hiddenServiceRemotePort, backupToPackage, + mKeyUri, authCookie + ); + } catch (RemoteException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + break; + } + } + }; + + String requestMsg = getString(R.string.hidden_service_request, hiddenServicePort); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(requestMsg).setPositiveButton("Allow", dialogClickListener) + .setNegativeButton("Deny", dialogClickListener).show(); + + return; //don't null the setIntent() as we need it later + + case INTENT_ACTION_REQUEST_START_TOR: + autoStartFromIntent = true; + + startTor(); + + //never allow backgrounds start from this type of intent start + //app devs who want background starts, can use the service intents + /** + if (Prefs.allowBackgroundStarts()) + { + Intent resultIntent; + if (lastStatusIntent == null) { + resultIntent = new Intent(intent); + } else { + resultIntent = lastStatusIntent; + } + resultIntent.putExtra(TorServiceConstants.EXTRA_STATUS, torStatus); + setResult(RESULT_OK, resultIntent); + finish(); + }*/ + + break; + case Intent.ACTION_VIEW: + String urlString = intent.getDataString(); + + if (urlString != null) { + + if (urlString.toLowerCase().startsWith("bridge://")) + + { + String newBridgeValue = urlString.substring(9); //remove the bridge protocol piece + newBridgeValue = URLDecoder.decode(newBridgeValue); //decode the value here + + showAlert(getString(R.string.bridges_updated), getString(R.string.restart_orbot_to_use_this_bridge_) + newBridgeValue, false); + + setNewBridges(newBridgeValue); + } + } + break; + } + + updateStatus(null); + + setIntent(null); + + } + + private void setNewBridges(String newBridgeValue) { + + Prefs.setBridgesList(newBridgeValue); //set the string to a preference + Prefs.putBridgesEnabled(true); + + setResult(RESULT_OK); + + mBtnBridges.setChecked(true); + + enableBridges(true); + } /* * Launch the system activity for Uri viewing with the provided url @@ -1193,11 +1287,7 @@ public class OrbotMainActivity extends AppCompatActivity if (autoStartFromIntent) { autoStartFromIntent = false; - Intent resultIntent = lastStatusIntent; - - if (resultIntent == null) - resultIntent = new Intent(TorServiceConstants.ACTION_START); - + Intent resultIntent = lastStatusIntent; resultIntent.putExtra(TorServiceConstants.EXTRA_STATUS, torStatus); setResult(RESULT_OK, resultIntent); finish(); diff --git a/app/src/main/java/org/torproject/android/settings/SettingsPreferences.java b/app/src/main/java/org/torproject/android/settings/SettingsPreferences.java index c14cb5a4..adacba72 100644 --- a/app/src/main/java/org/torproject/android/settings/SettingsPreferences.java +++ b/app/src/main/java/org/torproject/android/settings/SettingsPreferences.java @@ -37,9 +37,6 @@ public class SettingsPreferences private Preference prefTransProxyFlush = null; private Preference prefTransProxyApps = null; - private CheckBoxPreference prefHiddenServices = null; - private EditTextPreference prefHiddenServicesPorts; - private EditTextPreference prefHiddenServicesHostname; private CheckBoxPreference prefRequestRoot = null; private ListPreference prefLocale = null; @@ -104,22 +101,14 @@ public class SettingsPreferences prefTransProxyApps.setOnPreferenceClickListener(this); prefCBTransProxy.setOnPreferenceClickListener(this); prefcBTransProxyAll.setOnPreferenceClickListener(this); - prefHiddenServices = (CheckBoxPreference) findPreference("pref_hs_enable"); - prefHiddenServices.setOnPreferenceClickListener(this); - prefHiddenServicesHostname = (EditTextPreference) findPreference("pref_hs_hostname"); - - + prefCBTransProxy.setEnabled(prefRequestRoot.isChecked()); prefcBTransProxyAll.setEnabled(prefCBTransProxy.isChecked()); prefcbTransTethering.setEnabled(prefCBTransProxy.isChecked()); if (prefCBTransProxy.isChecked()) prefTransProxyApps.setEnabled((!prefcBTransProxyAll.isChecked())); - - prefHiddenServicesPorts = (EditTextPreference) findPreference("pref_hs_ports"); - prefHiddenServicesHostname.setEnabled(prefHiddenServices.isChecked()); - prefHiddenServicesPorts.setEnabled(prefHiddenServices.isChecked()); - + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { prefTransProxyApps.setEnabled(true); @@ -153,11 +142,6 @@ public class SettingsPreferences { startActivity(new Intent(this, AppManager.class)); - } - else if (preference == prefHiddenServices) - { - prefHiddenServicesPorts.setEnabled(prefHiddenServices.isChecked()); - prefHiddenServicesHostname.setEnabled(prefHiddenServices.isChecked()); } else { diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/ClientCookiesActivity.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/ClientCookiesActivity.java new file mode 100644 index 00000000..a766a5c7 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/ClientCookiesActivity.java @@ -0,0 +1,199 @@ +package org.torproject.android.ui.hiddenservices; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Handler; +import android.support.design.widget.FloatingActionButton; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ListView; +import android.widget.Toast; + +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; + +import org.json.JSONException; +import org.json.JSONObject; +import org.torproject.android.R; +import org.torproject.android.ui.hiddenservices.adapters.ClientCookiesAdapter; +import org.torproject.android.ui.hiddenservices.dialogs.AddCookieDialog; +import org.torproject.android.ui.hiddenservices.dialogs.CookieActionsDialog; +import org.torproject.android.ui.hiddenservices.dialogs.SelectCookieBackupDialog; +import org.torproject.android.ui.hiddenservices.permissions.PermissionManager; +import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider; + +public class ClientCookiesActivity extends AppCompatActivity { + public final int WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTIONBAR = 3; + + private ContentResolver mResolver; + private ClientCookiesAdapter mAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.layout_activity_client_cookies); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + mResolver = getContentResolver(); + + FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + AddCookieDialog dialog = new AddCookieDialog(); + dialog.show(getSupportFragmentManager(), "AddCookieDialog"); + } + }); + + mAdapter = new ClientCookiesAdapter( + this, + mResolver.query(CookieContentProvider.CONTENT_URI, CookieContentProvider.PROJECTION, null, null, null) + , 0); + + mResolver.registerContentObserver( + CookieContentProvider.CONTENT_URI, true, new HSObserver(new Handler()) + ); + + ListView cookies = (ListView) findViewById(R.id.clien_cookies_list); + cookies.setAdapter(mAdapter); + + cookies.setOnItemClickListener(new AdapterView.OnItemClickListener() { + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Cursor item = (Cursor) parent.getItemAtPosition(position); + + Bundle arguments = new Bundle(); + arguments.putInt( + "_id", item.getInt(item.getColumnIndex(CookieContentProvider.ClientCookie._ID)) + ); + + arguments.putString( + "domain", item.getString(item.getColumnIndex(CookieContentProvider.ClientCookie.DOMAIN)) + ); + + arguments.putString( + "auth_cookie_value", item.getString(item.getColumnIndex(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE)) + ); + + arguments.putInt( + "enabled", item.getInt(item.getColumnIndex(CookieContentProvider.ClientCookie.ENABLED)) + ); + + CookieActionsDialog dialog = new CookieActionsDialog(); + dialog.setArguments(arguments); + dialog.show(getSupportFragmentManager(), "CookieActionsDialog"); + } + }); + + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.cookie_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + if (id == R.id.cookie_restore_backup) { + if (PermissionManager.isLollipopOrHigher() + && !PermissionManager.hasExternalWritePermission(this)) { + PermissionManager.requestExternalWritePermissions(this, WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTIONBAR); + return true; + } + + SelectCookieBackupDialog dialog = new SelectCookieBackupDialog(); + dialog.show(getSupportFragmentManager(), "SelectCookieBackupDialog"); + + } else if (id == R.id.cookie_from_qr) { + IntentIntegrator integrator = new IntentIntegrator(ClientCookiesActivity.this); + integrator.initiateScan(); + } + + return super.onOptionsItemSelected(item); + } + + @Override + public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + if (grantResults.length < 1 + || grantResults[0] != PackageManager.PERMISSION_GRANTED) { + return; + } + + switch (requestCode) { + case WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTIONBAR: { + SelectCookieBackupDialog dialog = new SelectCookieBackupDialog(); + dialog.show(getSupportFragmentManager(), "SelectCookieBackupDialog"); + break; + } + case CookieActionsDialog.WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTION_DIALOG: { + Toast.makeText(this, R.string.click_again_for_backup, Toast.LENGTH_LONG).show(); + break; + } + } + } + + @Override + protected void onActivityResult(int request, int response, Intent data) { + super.onActivityResult(request, response, data); + + IntentResult scanResult = IntentIntegrator.parseActivityResult(request, response, data); + + if (scanResult == null) return; + + String results = scanResult.getContents(); + + if (results == null || results.length() < 1) return; + + try { + JSONObject savedValues = new JSONObject(results); + ContentValues fields = new ContentValues(); + + fields.put( + CookieContentProvider.ClientCookie.DOMAIN, + savedValues.getString(CookieContentProvider.ClientCookie.DOMAIN) + ); + + fields.put( + CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE, + savedValues.getString(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE) + ); + + mResolver.insert(CookieContentProvider.CONTENT_URI, fields); + + } catch (JSONException e) { + e.printStackTrace(); + Toast.makeText(this, R.string.error, Toast.LENGTH_LONG).show(); + } + } + + class HSObserver extends ContentObserver { + HSObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + mAdapter.changeCursor(mResolver.query( + CookieContentProvider.CONTENT_URI, CookieContentProvider.PROJECTION, null, null, null + )); + } + } + +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java new file mode 100644 index 00000000..f6445588 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java @@ -0,0 +1,214 @@ +package org.torproject.android.ui.hiddenservices; + + +import android.content.ContentResolver; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Handler; +import android.support.design.widget.FloatingActionButton; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.Spinner; +import android.widget.Toast; + +import org.torproject.android.R; +import org.torproject.android.ui.hiddenservices.adapters.OnionListAdapter; +import org.torproject.android.ui.hiddenservices.dialogs.HSActionsDialog; +import org.torproject.android.ui.hiddenservices.dialogs.HSDataDialog; +import org.torproject.android.ui.hiddenservices.dialogs.SelectHSBackupDialog; +import org.torproject.android.ui.hiddenservices.permissions.PermissionManager; +import org.torproject.android.ui.hiddenservices.providers.HSContentProvider; + +public class HiddenServicesActivity extends AppCompatActivity { + public final int WRITE_EXTERNAL_STORAGE_FROM_ACTIONBAR = 1; + private ContentResolver mResolver; + private OnionListAdapter mAdapter; + private FloatingActionButton fab; + + private String mWhere = HSContentProvider.HiddenService.CREATED_BY_USER + "=1"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.layout_hs_list_view); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + mResolver = getContentResolver(); + + fab = (FloatingActionButton) findViewById(R.id.fab); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + HSDataDialog dialog = new HSDataDialog(); + dialog.show(getSupportFragmentManager(), "HSDataDialog"); + } + }); + + mAdapter = new OnionListAdapter( + this, + mResolver.query( + HSContentProvider.CONTENT_URI, HSContentProvider.PROJECTION, mWhere, null, null + ), + 0 + ); + + mResolver.registerContentObserver( + HSContentProvider.CONTENT_URI, true, new HSObserver(new Handler()) + ); + + ListView onion_list = (ListView) findViewById(R.id.onion_list); + onion_list.setAdapter(mAdapter); + + onion_list.setOnItemClickListener(new AdapterView.OnItemClickListener() { + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Cursor item = (Cursor) parent.getItemAtPosition(position); + + Bundle arguments = new Bundle(); + arguments.putInt( + "_id", item.getInt(item.getColumnIndex(HSContentProvider.HiddenService._ID)) + ); + + arguments.putString( + "port", item.getString(item.getColumnIndex(HSContentProvider.HiddenService.PORT)) + ); + + arguments.putString( + "onion", item.getString(item.getColumnIndex(HSContentProvider.HiddenService.DOMAIN)) + ); + + arguments.putInt( + "auth_cookie", item.getInt(item.getColumnIndex(HSContentProvider.HiddenService.AUTH_COOKIE)) + ); + + arguments.putString( + "auth_cookie_value", item.getString(item.getColumnIndex(HSContentProvider.HiddenService.AUTH_COOKIE_VALUE)) + ); + + HSActionsDialog dialog = new HSActionsDialog(); + dialog.setArguments(arguments); + dialog.show(getSupportFragmentManager(), "HSActionsDialog"); + } + }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.hs_menu, menu); + + MenuItem item = menu.findItem(R.id.hs_type); + Spinner spinner = (Spinner) MenuItemCompat.getActionView(item); + + ArrayAdapter adapter = ArrayAdapter.createFromResource( + this, R.array.array_hs_types, android.R.layout.simple_spinner_item); + + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + spinner.setAdapter(adapter); + + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + + @Override + public void onItemSelected(AdapterView arg0, View v, int pos, long id) { + if (pos == 0) { + mWhere = HSContentProvider.HiddenService.CREATED_BY_USER + "=1"; + fab.show(); + } else { + mWhere = HSContentProvider.HiddenService.CREATED_BY_USER + "=0"; + fab.hide(); + } + + mAdapter.changeCursor(mResolver.query( + HSContentProvider.CONTENT_URI, HSContentProvider.PROJECTION, mWhere, null, null + )); + } + + @Override + public void onNothingSelected(AdapterView arg0) { + // Do nothing + } + }); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + if (id == R.id.menu_restore_backup) { + if (PermissionManager.isLollipopOrHigher() + && !PermissionManager.hasExternalWritePermission(this)) { + PermissionManager.requestExternalWritePermissions(this, WRITE_EXTERNAL_STORAGE_FROM_ACTIONBAR); + return true; + } + + SelectHSBackupDialog dialog = new SelectHSBackupDialog(); + dialog.show(getSupportFragmentManager(), "SelectHSBackupDialog"); + } + + return super.onOptionsItemSelected(item); + } + + @Override + public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + if (grantResults.length < 1 + || grantResults[0] != PackageManager.PERMISSION_GRANTED) { + return; + } + + switch (requestCode) { + case WRITE_EXTERNAL_STORAGE_FROM_ACTIONBAR: { + SelectHSBackupDialog dialog = new SelectHSBackupDialog(); + dialog.show(getSupportFragmentManager(), "SelectHSBackupDialog"); + break; + } + case HSActionsDialog.WRITE_EXTERNAL_STORAGE_FROM_ACTION_DIALOG: { + Toast.makeText(this, R.string.click_again_for_backup, Toast.LENGTH_LONG).show(); + break; + } + } + } + + class HSObserver extends ContentObserver { + HSObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + mAdapter.changeCursor(mResolver.query( + HSContentProvider.CONTENT_URI, HSContentProvider.PROJECTION, mWhere, null, null + )); + + if (PermissionManager.isLollipopOrHigher()) { + Cursor active = mResolver.query( + HSContentProvider.CONTENT_URI, HSContentProvider.PROJECTION, HSContentProvider.HiddenService.ENABLED + "=1", null, null + ); + + if (active == null) return; + + if (active.getCount() > 0) // Call only if there running services + PermissionManager.requestBatteryPermmssions(HiddenServicesActivity.this, getApplicationContext()); + else // Drop whe not needed + PermissionManager.requestDropBatteryPermmssions(HiddenServicesActivity.this, getApplicationContext()); + + active.close(); + } + } + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/BackupAdapter.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/BackupAdapter.java new file mode 100644 index 00000000..196023b3 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/BackupAdapter.java @@ -0,0 +1,50 @@ +package org.torproject.android.ui.hiddenservices.adapters; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import org.torproject.android.R; + +import java.io.File; +import java.util.List; + +public class BackupAdapter extends ArrayAdapter { + private int mResource; + + public BackupAdapter(Context context, int resource) { + super(context, resource); + mResource = resource; + } + + public BackupAdapter(Context context, int resource, List zips) { + super(context, resource, zips); + mResource = resource; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + + View v = convertView; + + if (v == null) { + LayoutInflater vi; + vi = LayoutInflater.from(getContext()); + v = vi.inflate(mResource, null); + } + + File p = getItem(position); + + if (p != null) { + TextView name = (TextView) v.findViewById(R.id.backup_name); + + if (name != null) + name.setText(p.getName()); + } + + return v; + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/ClientCookiesAdapter.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/ClientCookiesAdapter.java new file mode 100644 index 00000000..bbc317a6 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/ClientCookiesAdapter.java @@ -0,0 +1,64 @@ +package org.torproject.android.ui.hiddenservices.adapters; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; + +import org.torproject.android.R; +import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider; + +public class ClientCookiesAdapter extends CursorAdapter { + private LayoutInflater cursorInflater; + + public ClientCookiesAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + + cursorInflater = (LayoutInflater) context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + final Context mContext = context; + int id = cursor.getInt(cursor.getColumnIndex(CookieContentProvider.ClientCookie._ID)); + final String where = CookieContentProvider.ClientCookie._ID + "=" + id; + + TextView domain = (TextView) view.findViewById(R.id.cookie_onion); + domain.setText(cursor.getString(cursor.getColumnIndex(CookieContentProvider.ClientCookie.DOMAIN))); + + Switch enabled = (Switch) view.findViewById(R.id.cookie_switch); + enabled.setChecked( + cursor.getInt(cursor.getColumnIndex(CookieContentProvider.ClientCookie.ENABLED)) == 1 + ); + + enabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + ContentResolver resolver = mContext.getContentResolver(); + ContentValues fields = new ContentValues(); + fields.put(CookieContentProvider.ClientCookie.ENABLED, isChecked); + resolver.update( + CookieContentProvider.CONTENT_URI, fields, where, null + ); + + Toast.makeText( + mContext, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG + ).show(); + } + }); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return cursorInflater.inflate(R.layout.layout_client_cookie_list_item, parent, false); + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/OnionListAdapter.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/OnionListAdapter.java new file mode 100644 index 00000000..9824c773 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/OnionListAdapter.java @@ -0,0 +1,68 @@ +package org.torproject.android.ui.hiddenservices.adapters; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; + +import org.torproject.android.R; +import org.torproject.android.ui.hiddenservices.providers.HSContentProvider; + +public class OnionListAdapter extends CursorAdapter { + private LayoutInflater cursorInflater; + + public OnionListAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + + cursorInflater = (LayoutInflater) context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + final Context mContext = context; + int id = cursor.getInt(cursor.getColumnIndex(HSContentProvider.HiddenService._ID)); + final String where = HSContentProvider.HiddenService._ID + "=" + id; + + TextView port = (TextView) view.findViewById(R.id.hs_port); + port.setText(cursor.getString(cursor.getColumnIndex(HSContentProvider.HiddenService.PORT))); + TextView name = (TextView) view.findViewById(R.id.hs_name); + name.setText(cursor.getString(cursor.getColumnIndex(HSContentProvider.HiddenService.NAME))); + TextView domain = (TextView) view.findViewById(R.id.hs_onion); + domain.setText(cursor.getString(cursor.getColumnIndex(HSContentProvider.HiddenService.DOMAIN))); + + Switch enabled = (Switch) view.findViewById(R.id.hs_switch); + enabled.setChecked( + cursor.getInt(cursor.getColumnIndex(HSContentProvider.HiddenService.ENABLED)) == 1 + ); + + enabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + ContentResolver resolver = mContext.getContentResolver(); + ContentValues fields = new ContentValues(); + fields.put(HSContentProvider.HiddenService.ENABLED, isChecked); + resolver.update( + HSContentProvider.CONTENT_URI, fields, where, null + ); + + Toast.makeText( + mContext, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG + ).show(); + } + }); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return cursorInflater.inflate(R.layout.layout_hs_list_item, parent, false); + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/BackupUtils.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/BackupUtils.java new file mode 100644 index 00000000..a4790dd8 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/BackupUtils.java @@ -0,0 +1,336 @@ +package org.torproject.android.ui.hiddenservices.backup; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.widget.Toast; + +import org.json.JSONException; +import org.json.JSONObject; +import org.torproject.android.R; +import org.torproject.android.service.TorServiceConstants; +import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider; +import org.torproject.android.ui.hiddenservices.providers.HSContentProvider; +import org.torproject.android.ui.hiddenservices.storage.ExternalStorage; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; + +public class BackupUtils { + private final String configFileName = "config.json"; + private Context mContext; + private ContentResolver mResolver; + + public BackupUtils(Context context) { + mContext = context; + mResolver = mContext.getContentResolver(); + } + + public String createZipBackup(Integer port) { + File mHSBasePath = new File( + mContext.getFilesDir().getAbsolutePath(), + TorServiceConstants.HIDDEN_SERVICES_DIR + ); + + String configFilePath = mHSBasePath + "/hs" + port + "/" + configFileName; + String hostnameFilePath = mHSBasePath + "/hs" + port + "/hostname"; + String keyFilePath = mHSBasePath + "/hs" + port + "/private_key"; + + File storage_path = ExternalStorage.getOrCreateBackupDir(); + + if (storage_path == null) + return null; + + Cursor portData = mResolver.query( + HSContentProvider.CONTENT_URI, + HSContentProvider.PROJECTION, + HSContentProvider.HiddenService.PORT + "=" + port, + null, + null + ); + + JSONObject config = new JSONObject(); + try { + if (portData == null || portData.getCount() != 1) + return null; + + portData.moveToNext(); + + config.put( + HSContentProvider.HiddenService.NAME, + portData.getString(portData.getColumnIndex(HSContentProvider.HiddenService.NAME)) + ); + + config.put( + HSContentProvider.HiddenService.PORT, + portData.getInt(portData.getColumnIndex(HSContentProvider.HiddenService.PORT)) + ); + + config.put( + HSContentProvider.HiddenService.ONION_PORT, + portData.getInt(portData.getColumnIndex(HSContentProvider.HiddenService.ONION_PORT)) + ); + + config.put( + HSContentProvider.HiddenService.DOMAIN, + portData.getString(portData.getColumnIndex(HSContentProvider.HiddenService.DOMAIN)) + ); + + config.put( + HSContentProvider.HiddenService.AUTH_COOKIE, + portData.getInt(portData.getColumnIndex(HSContentProvider.HiddenService.AUTH_COOKIE)) + ); + + config.put( + HSContentProvider.HiddenService.AUTH_COOKIE_VALUE, + portData.getString(portData.getColumnIndex(HSContentProvider.HiddenService.AUTH_COOKIE_VALUE)) + ); + + config.put( + HSContentProvider.HiddenService.CREATED_BY_USER, + portData.getInt(portData.getColumnIndex(HSContentProvider.HiddenService.CREATED_BY_USER)) + ); + + config.put( + HSContentProvider.HiddenService.ENABLED, + portData.getInt(portData.getColumnIndex(HSContentProvider.HiddenService.ENABLED)) + ); + } catch (JSONException e) { + e.printStackTrace(); + return null; + } catch (NullPointerException e) { + e.printStackTrace(); + return null; + } + + portData.close(); + + try { + FileWriter file = new FileWriter(configFilePath); + file.write(config.toString()); + file.close(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + + String zip_path = storage_path.getAbsolutePath() + "/hs" + port + ".zip"; + String files[] = {hostnameFilePath, keyFilePath, configFilePath}; + + ZipIt zip = new ZipIt(files, zip_path); + + if (!zip.zip()) + return null; + + return zip_path; + } + + public void restoreZipBackup(File backup) { + + File mHSBasePath = new File( + mContext.getFilesDir().getAbsolutePath(), + TorServiceConstants.HIDDEN_SERVICES_DIR + ); + + int port; + Cursor service; + String backupName = backup.getName(); + String hsDir = backupName.substring(0, backupName.lastIndexOf('.')); + String configFilePath = mHSBasePath + "/" + hsDir + "/" + configFileName; + String jString = null; + + File hsPath = new File(mHSBasePath.getAbsolutePath(), hsDir); + if (!hsPath.isDirectory()) + hsPath.mkdirs(); + + ZipIt zip = new ZipIt(null, backup.getAbsolutePath()); + zip.unzip(hsPath.getAbsolutePath()); + + File config = new File(configFilePath); + FileInputStream stream; + + try { + stream = new FileInputStream(config); + FileChannel fc = stream.getChannel(); + MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()); + jString = Charset.defaultCharset().decode(bb).toString(); + stream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + if (jString == null) + Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show(); + + try { + JSONObject savedValues = new JSONObject(jString); + ContentValues fields = new ContentValues(); + + fields.put( + HSContentProvider.HiddenService.NAME, + savedValues.getString(HSContentProvider.HiddenService.NAME) + ); + + fields.put( + HSContentProvider.HiddenService.ONION_PORT, + savedValues.getInt(HSContentProvider.HiddenService.ONION_PORT) + ); + + fields.put( + HSContentProvider.HiddenService.DOMAIN, + savedValues.getString(HSContentProvider.HiddenService.DOMAIN) + ); + + fields.put( + HSContentProvider.HiddenService.AUTH_COOKIE, + savedValues.getInt(HSContentProvider.HiddenService.AUTH_COOKIE) + ); + + fields.put( + HSContentProvider.HiddenService.CREATED_BY_USER, + savedValues.getInt(HSContentProvider.HiddenService.CREATED_BY_USER) + ); + + fields.put( + HSContentProvider.HiddenService.ENABLED, + savedValues.getInt(HSContentProvider.HiddenService.ENABLED) + ); + + port = savedValues.getInt(HSContentProvider.HiddenService.PORT); + fields.put(HSContentProvider.HiddenService.PORT, port); + + service = mResolver.query( + HSContentProvider.CONTENT_URI, + HSContentProvider.PROJECTION, + HSContentProvider.HiddenService.PORT + "=" + port, + null, + null + ); + + if (service == null || service.getCount() == 0) { + mResolver.insert(HSContentProvider.CONTENT_URI, fields); + } else { + mResolver.update( + HSContentProvider.CONTENT_URI, + fields, + HSContentProvider.HiddenService.PORT + "=" + port, + null + ); + + service.close(); + } + + } catch (JSONException e) { + e.printStackTrace(); + Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show(); + } + + Toast.makeText(mContext, R.string.backup_restored, Toast.LENGTH_LONG).show(); + } + + public void restoreKeyBackup(int hsPort, Uri hsKeyPath) { + File mHSBasePath = new File( + mContext.getFilesDir().getAbsolutePath(), + TorServiceConstants.HIDDEN_SERVICES_DIR + ); + + File serviceDir = new File(mHSBasePath, "hs" + hsPort); + + if (!serviceDir.isDirectory()) + serviceDir.mkdirs(); + + try { + ParcelFileDescriptor mInputPFD = mContext.getContentResolver().openFileDescriptor(hsKeyPath, "r"); + InputStream fileStream = new FileInputStream(mInputPFD.getFileDescriptor()); + OutputStream file = new FileOutputStream(serviceDir.getAbsolutePath() + "/private_key"); + + byte[] buffer = new byte[1024]; + int length; + while ((length = fileStream.read(buffer)) > 0) { + file.write(buffer, 0, length); + } + file.close(); + + } catch (IOException | NullPointerException e) { + e.printStackTrace(); + } + } + + public void restoreCookieBackup(File p) { + File config = new File(p.getAbsolutePath()); + FileInputStream stream; + String jString = null; + + try { + stream = new FileInputStream(config); + FileChannel fc = stream.getChannel(); + MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()); + jString = Charset.defaultCharset().decode(bb).toString(); + stream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + if (jString == null) + Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show(); + + try { + JSONObject savedValues = new JSONObject(jString); + ContentValues fields = new ContentValues(); + + fields.put( + CookieContentProvider.ClientCookie.DOMAIN, + savedValues.getString(CookieContentProvider.ClientCookie.DOMAIN) + ); + + fields.put( + CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE, + savedValues.getString(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE) + ); + + fields.put( + CookieContentProvider.ClientCookie.ENABLED, + savedValues.getInt(CookieContentProvider.ClientCookie.ENABLED) + ); + + mResolver.insert(CookieContentProvider.CONTENT_URI, fields); + + } catch (JSONException e) { + e.printStackTrace(); + Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show(); + } + + Toast.makeText(mContext, R.string.backup_restored, Toast.LENGTH_LONG).show(); + } + + public String createCookieBackup(String domain, String cookie, Integer enabled) { + File storage_path = ExternalStorage.getOrCreateBackupDir(); + String backupFile = storage_path.getAbsolutePath() + '/' + domain.replace(".onion", ".json"); + + JSONObject backup = new JSONObject(); + try { + backup.put(CookieContentProvider.ClientCookie.DOMAIN, domain); + backup.put(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE, cookie); + backup.put(CookieContentProvider.ClientCookie.ENABLED, enabled); + FileWriter file = new FileWriter(backupFile); + file.write(backup.toString()); + file.close(); + } catch (JSONException | IOException e) { + e.printStackTrace(); + return null; + } + + return backupFile; + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/ZipIt.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/ZipIt.java new file mode 100644 index 00000000..130b56bb --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/ZipIt.java @@ -0,0 +1,101 @@ +package org.torproject.android.ui.hiddenservices.backup; + + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +public class ZipIt { + private static final int BUFFER = 2048; + + private String[] _files; + private String _zipFile; + + public ZipIt(@Nullable String[] files, @NonNull String zipFile) { + _files = files; + _zipFile = zipFile; + } + + public boolean zip() { + try { + BufferedInputStream origin; + FileOutputStream dest = new FileOutputStream(_zipFile); + + ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(dest)); + + byte data[] = new byte[BUFFER]; + + for (String _file : _files) { + FileInputStream fi = new FileInputStream(_file); + origin = new BufferedInputStream(fi, BUFFER); + ZipEntry entry = new ZipEntry(_file.substring(_file.lastIndexOf("/") + 1)); + out.putNextEntry(entry); + int count; + while ((count = origin.read(data, 0, BUFFER)) != -1) { + out.write(data, 0, count); + } + origin.close(); + } + + out.close(); + } catch (Exception e) { + return false; + } + + return true; + } + + public boolean unzip(String output_path) { + InputStream is; + ZipInputStream zis; + + try { + String filename; + is = new FileInputStream(_zipFile); + zis = new ZipInputStream(new BufferedInputStream(is)); + ZipEntry ze; + byte[] buffer = new byte[1024]; + int count; + + while ((ze = zis.getNextEntry()) != null) { + // zapis do souboru + filename = ze.getName(); + + // Need to create directories if not exists, or + // it will generate an Exception... + if (ze.isDirectory()) { + File fmd = new File(output_path + "/" + filename); + fmd.mkdirs(); + continue; + } + + FileOutputStream fout = new FileOutputStream(output_path + "/" + filename); + + // cteni zipu a zapis + while ((count = zis.read(buffer)) != -1) { + fout.write(buffer, 0, count); + } + + fout.close(); + zis.closeEntry(); + } + + zis.close(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/database/HSDatabase.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/database/HSDatabase.java new file mode 100644 index 00000000..18bc7aa0 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/database/HSDatabase.java @@ -0,0 +1,47 @@ +package org.torproject.android.ui.hiddenservices.database; + + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class HSDatabase extends SQLiteOpenHelper { + + public static final String HS_DATA_TABLE_NAME = "hs_data"; + public static final String HS_CLIENT_COOKIE_TABLE_NAME = "hs_client_cookie"; + private static final int DATABASE_VERSION = 2; + private static final String DATABASE_NAME = "hidden_services"; + private static final String HS_DATA_TABLE_CREATE = + "CREATE TABLE " + HS_DATA_TABLE_NAME + " (" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "name TEXT, " + + "domain TEXT, " + + "onion_port INTEGER, " + + "auth_cookie INTEGER DEFAULT 0, " + + "auth_cookie_value TEXT, " + + "created_by_user INTEGER DEFAULT 0, " + + "enabled INTEGER DEFAULT 1, " + + "port INTEGER);"; + + private static final String HS_CLIENT_COOKIE_TABLE_CREATE = + "CREATE TABLE " + HS_CLIENT_COOKIE_TABLE_NAME + " (" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "domain TEXT, " + + "auth_cookie_value TEXT, " + + "enabled INTEGER DEFAULT 1);"; + + public HSDatabase(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(HS_DATA_TABLE_CREATE); + db.execSQL(HS_CLIENT_COOKIE_TABLE_CREATE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + } +} + diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/AddCookieDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/AddCookieDialog.java new file mode 100644 index 00000000..75663648 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/AddCookieDialog.java @@ -0,0 +1,84 @@ +package org.torproject.android.ui.hiddenservices.dialogs; + + +import android.app.Dialog; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import org.torproject.android.R; +import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider; + +public class AddCookieDialog extends DialogFragment { + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + + final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_add_client_cookie_dialog, null); + + final AlertDialog addCookieDialog = new AlertDialog.Builder(getActivity()) + .setView(dialog_view) + .setTitle(R.string.client_cookies) + .create(); + + Button save = (Button) dialog_view.findViewById(R.id.cookie_dialog_save); + save.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + String onion = ((EditText) dialog_view.findViewById(R.id.cookie_onion)).getText().toString(); + String cookie = ((EditText) dialog_view.findViewById(R.id.cookie_value)).getText().toString(); + + if (checkInput(onion, cookie)) { + saveData(onion, cookie); + Toast.makeText( + v.getContext(), R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG + ).show(); + addCookieDialog.dismiss(); + } + } + }); + + Button cancel = (Button) dialog_view.findViewById(R.id.cookie_dialog_cancel); + cancel.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + addCookieDialog.cancel(); + } + }); + + return addCookieDialog; + } + + private boolean checkInput(String onion, String cookie) { + + boolean is_set = ((onion != null && onion.length() > 0) && (cookie != null && cookie.length() > 0)); + if (!is_set) { + Toast.makeText(getContext(), R.string.fields_can_t_be_empty, Toast.LENGTH_SHORT).show(); + return false; + } + + if (!onion.matches("([a-z0-9]{16})\\.onion")) { + Toast.makeText(getContext(), R.string.invalid_onion_address, Toast.LENGTH_SHORT).show(); + return false; + } + + return true; + } + + private void saveData(String domain, String cookie) { + + ContentValues fields = new ContentValues(); + fields.put(CookieContentProvider.ClientCookie.DOMAIN, domain); + fields.put(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE, cookie); + + ContentResolver cr = getContext().getContentResolver(); + + cr.insert(CookieContentProvider.CONTENT_URI, fields); + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieActionsDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieActionsDialog.java new file mode 100644 index 00000000..7b5a2cfe --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieActionsDialog.java @@ -0,0 +1,95 @@ +package org.torproject.android.ui.hiddenservices.dialogs; + + +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; + +import org.torproject.android.R; +import org.torproject.android.ui.hiddenservices.backup.BackupUtils; +import org.torproject.android.ui.hiddenservices.permissions.PermissionManager; + +public class CookieActionsDialog extends DialogFragment { + public static final int WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTION_DIALOG = 4; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Bundle arguments = getArguments(); + + final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_cookie_actions, null); + final AlertDialog actionDialog = new AlertDialog.Builder(getActivity()) + .setView(dialog_view) + .setTitle(R.string.client_cookies) + .create(); + + Button backup = (Button) dialog_view.findViewById(R.id.btn_cookie_backup); + backup.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + Context mContext = v.getContext(); + + if (PermissionManager.isLollipopOrHigher() + && !PermissionManager.hasExternalWritePermission(mContext)) { + + PermissionManager.requestExternalWritePermissions( + getActivity(), WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTION_DIALOG); + + return; + } + + BackupUtils backup_utils = new BackupUtils(mContext); + String backupPath = backup_utils.createCookieBackup( + arguments.getString("domain"), + arguments.getString("auth_cookie_value"), + arguments.getInt("enabled") + ); + + if (backupPath == null || backupPath.length() < 1) { + Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show(); + actionDialog.dismiss(); + return; + } + + Toast.makeText(mContext, R.string.backup_saved_at_external_storage, Toast.LENGTH_LONG).show(); + + Uri selectedUri = Uri.parse(backupPath.substring(0, backupPath.lastIndexOf("/"))); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(selectedUri, "resource/folder"); + + if (intent.resolveActivityInfo(mContext.getPackageManager(), 0) != null) { + startActivity(intent); + } else { + Toast.makeText(mContext, R.string.filemanager_not_available, Toast.LENGTH_LONG).show(); + } + actionDialog.dismiss(); + } + }); + + Button delete = (Button) dialog_view.findViewById(R.id.btn_cookie_delete); + delete.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + CookieDeleteDialog dialog = new CookieDeleteDialog(); + dialog.setArguments(arguments); + dialog.show(getFragmentManager(), "CookieDeleteDialog"); + actionDialog.dismiss(); + } + }); + + Button cancel = (Button) dialog_view.findViewById(R.id.btn_cookie_cancel); + cancel.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + actionDialog.dismiss(); + } + }); + + return actionDialog; + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieDeleteDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieDeleteDialog.java new file mode 100644 index 00000000..60c4d8e3 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieDeleteDialog.java @@ -0,0 +1,50 @@ +package org.torproject.android.ui.hiddenservices.dialogs; + + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; + +import org.torproject.android.R; +import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider; + +public class CookieDeleteDialog extends DialogFragment { + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Bundle arguments = getArguments(); + final Context context = getContext(); + + DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + // Delete from db + context.getContentResolver().delete( + CookieContentProvider.CONTENT_URI, + CookieContentProvider.ClientCookie._ID + "=" + arguments.getInt("_id"), + null + ); + + break; + + case DialogInterface.BUTTON_NEGATIVE: + // Do nothing + break; + } + } + }; + + return new AlertDialog.Builder(context) + .setMessage(R.string.confirm_cookie_deletion) + .setPositiveButton(R.string.btn_okay, dialogClickListener) + .setNegativeButton(R.string.btn_cancel, dialogClickListener) + .create(); + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSActionsDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSActionsDialog.java new file mode 100644 index 00000000..5be76917 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSActionsDialog.java @@ -0,0 +1,130 @@ +package org.torproject.android.ui.hiddenservices.dialogs; + + +import android.app.Dialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; + +import org.torproject.android.R; +import org.torproject.android.ui.hiddenservices.backup.BackupUtils; +import org.torproject.android.ui.hiddenservices.permissions.PermissionManager; + +public class HSActionsDialog extends DialogFragment { + public static final int WRITE_EXTERNAL_STORAGE_FROM_ACTION_DIALOG = 2; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Bundle arguments = getArguments(); + + final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_hs_actions, null); + final AlertDialog actionDialog = new AlertDialog.Builder(getActivity()) + .setView(dialog_view) + .setTitle(R.string.hidden_services) + .create(); + + Button backup = (Button) dialog_view.findViewById(R.id.btn_hs_backup); + backup.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + Context mContext = v.getContext(); + + if (PermissionManager.isLollipopOrHigher() + && !PermissionManager.hasExternalWritePermission(mContext)) { + + PermissionManager.requestExternalWritePermissions( + getActivity(), WRITE_EXTERNAL_STORAGE_FROM_ACTION_DIALOG); + + return; + } + + BackupUtils hsutils = new BackupUtils(mContext); + String backupPath = hsutils.createZipBackup(Integer.parseInt(arguments.getString("port"))); + + if (backupPath == null || backupPath.length() < 1) { + Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show(); + actionDialog.dismiss(); + return; + } + + Toast.makeText(mContext, R.string.backup_saved_at_external_storage, Toast.LENGTH_LONG).show(); + + Uri selectedUri = Uri.parse(backupPath.substring(0, backupPath.lastIndexOf("/"))); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(selectedUri, "resource/folder"); + + if (intent.resolveActivityInfo(mContext.getPackageManager(), 0) != null) { + startActivity(intent); + } else { + Toast.makeText(mContext, R.string.filemanager_not_available, Toast.LENGTH_LONG).show(); + } + actionDialog.dismiss(); + } + }); + + Button copy = (Button) dialog_view.findViewById(R.id.btn_hs_clipboard); + copy.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + Context mContext = v.getContext(); + ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("onion", arguments.getString("onion")); + clipboard.setPrimaryClip(clip); + Toast.makeText(mContext, R.string.done, Toast.LENGTH_LONG).show(); + actionDialog.dismiss(); + } + }); + + Button showAuth = (Button) dialog_view.findViewById(R.id.bt_hs_show_auth); + showAuth.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + String auth_cookie_value = arguments.getString("auth_cookie_value"); + + if (arguments.getInt("auth_cookie") == 1) { + if (auth_cookie_value == null || auth_cookie_value.length() < 1) { + Toast.makeText( + v.getContext(), R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG + ).show(); + } else { + HSCookieDialog dialog = new HSCookieDialog(); + dialog.setArguments(arguments); + dialog.show(getFragmentManager(), "HSCookieDialog"); + } + } else { + Toast.makeText( + v.getContext(), R.string.auth_cookie_was_not_configured, Toast.LENGTH_LONG + ).show(); + } + + actionDialog.dismiss(); + } + }); + + Button delete = (Button) dialog_view.findViewById(R.id.btn_hs_delete); + delete.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + HSDeleteDialog dialog = new HSDeleteDialog(); + dialog.setArguments(arguments); + dialog.show(getFragmentManager(), "HSDeleteDialog"); + actionDialog.dismiss(); + } + }); + + Button cancel = (Button) dialog_view.findViewById(R.id.btn_hs_cancel); + cancel.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + actionDialog.dismiss(); + } + }); + + return actionDialog; + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSCookieDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSCookieDialog.java new file mode 100644 index 00000000..39c1b510 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSCookieDialog.java @@ -0,0 +1,81 @@ +package org.torproject.android.ui.hiddenservices.dialogs; + +import android.app.Dialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.zxing.integration.android.IntentIntegrator; + +import org.json.JSONException; +import org.json.JSONObject; +import org.torproject.android.R; +import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider; + +public class HSCookieDialog extends DialogFragment { + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_hs_cookie, null); + + final Bundle arguments = getArguments(); + final String auth_cookie_value = arguments.getString("auth_cookie_value"); + + final AlertDialog cookieDialog = new AlertDialog.Builder(getActivity()) + .setView(dialog_view) + .create(); + + TextView cookie = (TextView) dialog_view.findViewById(R.id.hs_cookie); + cookie.setText(auth_cookie_value); + + Button clipboard = (Button) dialog_view.findViewById(R.id.hs_cookie_to_clipboard); + clipboard.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + Context mContext = v.getContext(); + ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("cookie", auth_cookie_value); + clipboard.setPrimaryClip(clip); + Toast.makeText(mContext, R.string.done, Toast.LENGTH_LONG).show(); + cookieDialog.dismiss(); + } + }); + + Button shareQR = (Button) dialog_view.findViewById(R.id.hs_cookie_to_qr); + shareQR.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + try { + JSONObject backup = new JSONObject(); + backup.put(CookieContentProvider.ClientCookie.DOMAIN, arguments.getString("onion")); + backup.put(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE, arguments.getString("auth_cookie_value")); + + IntentIntegrator integrator = new IntentIntegrator(getActivity()); + integrator.shareText(backup.toString()); + + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + cookieDialog.dismiss(); + } + }); + + Button cancel = (Button) dialog_view.findViewById(R.id.hs_cookie_cancel); + cancel.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + cookieDialog.dismiss(); + } + }); + + return cookieDialog; + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSDataDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSDataDialog.java new file mode 100644 index 00000000..98255604 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSDataDialog.java @@ -0,0 +1,102 @@ +package org.torproject.android.ui.hiddenservices.dialogs; + + +import android.app.Dialog; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.Toast; + +import org.torproject.android.R; +import org.torproject.android.ui.hiddenservices.providers.HSContentProvider; + +public class HSDataDialog extends DialogFragment { + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + // Get the layout + final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_hs_data_dialog, null); + + // Use the Builder class for convenient dialog construction + final AlertDialog serviceDataDialog = new AlertDialog.Builder(getActivity()) + .setView(dialog_view) + .setTitle(R.string.hidden_services) + .create(); + + // Buttons action + Button save = (Button) dialog_view.findViewById(R.id.HSDialogSave); + save.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + String serverName = ((EditText) dialog_view.findViewById(R.id.hsName)).getText().toString(); + Integer localPort = Integer.parseInt( + ((EditText) dialog_view.findViewById(R.id.hsLocalPort)).getText().toString() + ); + Integer onionPort = Integer.parseInt( + ((EditText) dialog_view.findViewById(R.id.hsOnionPort)).getText().toString() + ); + + Boolean authCookie = ((CheckBox) dialog_view.findViewById(R.id.hsAuth)).isChecked(); + + if (checkInput(serverName, localPort, onionPort)) { + saveData(serverName, localPort, onionPort, authCookie); + Toast.makeText( + v.getContext(), R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG + ).show(); + serviceDataDialog.dismiss(); + } + } + }); + + Button cancel = (Button) dialog_view.findViewById(R.id.HSDialogCancel); + cancel.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + serviceDataDialog.cancel(); + } + }); + + return serviceDataDialog; + } + + private boolean checkInput(String serverName, Integer local, Integer remote) { + boolean is_ok = true; + Integer error_msg = 0; + + if ((local < 1 || local > 65535) || (remote < 1 || remote > 65535)) { + error_msg = R.string.invalid_port; + is_ok = false; + } + + if (serverName == null || serverName.length() < 1) { + error_msg = R.string.name_can_t_be_empty; + is_ok = false; + } + + if (!is_ok) { + Toast.makeText(getContext(), error_msg, Toast.LENGTH_SHORT).show(); + } + + return is_ok; + } + + private void saveData(String name, Integer local, Integer remote, Boolean authCookie) { + + ContentValues fields = new ContentValues(); + fields.put(HSContentProvider.HiddenService.NAME, name); + fields.put(HSContentProvider.HiddenService.PORT, local); + fields.put(HSContentProvider.HiddenService.ONION_PORT, remote); + fields.put(HSContentProvider.HiddenService.AUTH_COOKIE, authCookie); + fields.put(HSContentProvider.HiddenService.CREATED_BY_USER, 1); + + ContentResolver cr = getContext().getContentResolver(); + + cr.insert(HSContentProvider.CONTENT_URI, fields); + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSDeleteDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSDeleteDialog.java new file mode 100644 index 00000000..ac5df743 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSDeleteDialog.java @@ -0,0 +1,65 @@ +package org.torproject.android.ui.hiddenservices.dialogs; + + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; + +import org.torproject.android.R; +import org.torproject.android.service.TorServiceConstants; +import org.torproject.android.ui.hiddenservices.providers.HSContentProvider; + +import java.io.File; + +public class HSDeleteDialog extends DialogFragment { + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Bundle arguments = getArguments(); + final Context context = getContext(); + + DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + // Delete from db + context.getContentResolver().delete( + HSContentProvider.CONTENT_URI, + HSContentProvider.HiddenService._ID + "=" + arguments.getInt("_id"), + null + ); + + // Delete from interal storage + String base = context.getFilesDir().getAbsolutePath() + "/" + TorServiceConstants.HIDDEN_SERVICES_DIR; + File dir = new File(base, "hs" + arguments.getString("port")); + + if (dir.isDirectory()) { + String[] children = dir.list(); + for (String aChildren : children) { + new File(dir, aChildren).delete(); + } + dir.delete(); + } + + break; + + case DialogInterface.BUTTON_NEGATIVE: + // Do nothing + break; + } + } + }; + + return new AlertDialog.Builder(context) + .setMessage(R.string.confirm_service_deletion) + .setPositiveButton(R.string.btn_okay, dialogClickListener) + .setNegativeButton(R.string.btn_cancel, dialogClickListener) + .create(); + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectCookieBackupDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectCookieBackupDialog.java new file mode 100644 index 00000000..de5b7848 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectCookieBackupDialog.java @@ -0,0 +1,84 @@ +package org.torproject.android.ui.hiddenservices.dialogs; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ListView; + +import org.torproject.android.R; +import org.torproject.android.ui.hiddenservices.adapters.BackupAdapter; +import org.torproject.android.ui.hiddenservices.backup.BackupUtils; +import org.torproject.android.ui.hiddenservices.storage.ExternalStorage; + +import java.io.File; +import java.io.FilenameFilter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SelectCookieBackupDialog extends DialogFragment { + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder cookieBackupDialog = new AlertDialog.Builder(getActivity()); + + cookieBackupDialog.setTitle(R.string.restore_backup); + + File backupDir = ExternalStorage.getOrCreateBackupDir(); + File[] files = null; + + try { + files = backupDir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.toLowerCase().endsWith(".json"); + } + }); + } catch (NullPointerException e) { + // Silent block + } + + if (files == null || files.length < 1) { + cookieBackupDialog.setMessage(R.string.create_a_backup_first); + cookieBackupDialog.setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }); + + return cookieBackupDialog.create(); + } + + final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_hs_backups_list, null); + + cookieBackupDialog.setView(dialog_view); + cookieBackupDialog.setPositiveButton(R.string.btn_okay, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }); + + ListView backups = (ListView) dialog_view.findViewById(R.id.listview_hs_backups); + + List json_backups = new ArrayList<>(); + Collections.addAll(json_backups, files); + + backups.setAdapter(new BackupAdapter(getContext(), R.layout.layout_hs_backups_list_item, json_backups)); + backups.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + BackupUtils backupUtils = new BackupUtils(view.getContext().getApplicationContext()); + File p = (File) parent.getItemAtPosition(position); + backupUtils.restoreCookieBackup(p); + } + }); + + return cookieBackupDialog.create(); + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectHSBackupDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectHSBackupDialog.java new file mode 100644 index 00000000..a6e3bac9 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectHSBackupDialog.java @@ -0,0 +1,84 @@ +package org.torproject.android.ui.hiddenservices.dialogs; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ListView; + +import org.torproject.android.R; +import org.torproject.android.ui.hiddenservices.adapters.BackupAdapter; +import org.torproject.android.ui.hiddenservices.backup.BackupUtils; +import org.torproject.android.ui.hiddenservices.storage.ExternalStorage; + +import java.io.File; +import java.io.FilenameFilter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SelectHSBackupDialog extends DialogFragment { + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder backupsDialog = new AlertDialog.Builder(getActivity()); + + backupsDialog.setTitle(R.string.restore_backup); + + File backupDir = ExternalStorage.getOrCreateBackupDir(); + File[] files = null; + + try { + files = backupDir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.toLowerCase().endsWith(".zip"); + } + }); + } catch (NullPointerException e) { + // Silent block + } + + if (files == null || files.length < 1) { + backupsDialog.setMessage(R.string.create_a_backup_first); + backupsDialog.setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }); + + return backupsDialog.create(); + } + + final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_hs_backups_list, null); + + backupsDialog.setView(dialog_view); + backupsDialog.setPositiveButton(R.string.btn_okay, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }); + + ListView backups = (ListView) dialog_view.findViewById(R.id.listview_hs_backups); + + List zips = new ArrayList<>(); + Collections.addAll(zips, files); + + backups.setAdapter(new BackupAdapter(getContext(), R.layout.layout_hs_backups_list_item, zips)); + backups.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + BackupUtils backupUtils = new BackupUtils(view.getContext().getApplicationContext()); + File p = (File) parent.getItemAtPosition(position); + backupUtils.restoreZipBackup(p); + } + }); + + return backupsDialog.create(); + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/permissions/PermissionManager.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/permissions/PermissionManager.java new file mode 100644 index 00000000..a9df5a95 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/permissions/PermissionManager.java @@ -0,0 +1,103 @@ +package org.torproject.android.ui.hiddenservices.permissions; + + +import android.Manifest; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.PowerManager; +import android.provider.Settings; +import android.support.design.widget.Snackbar; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.FragmentActivity; +import android.view.View; + +import org.torproject.android.R; + +public class PermissionManager { + public static int VERY_LONG_LENGTH = 6000; + + public static boolean isLollipopOrHigher() { + return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP); + } + + @SuppressLint("NewApi") + public static boolean hasExternalWritePermission(Context context) { + return (context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED); + } + + public static void requestExternalWritePermissions(FragmentActivity activity, int action) { + final int mAction = action; + final FragmentActivity mActivity = activity; + + if (ActivityCompat.shouldShowRequestPermissionRationale + (mActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + Snackbar.make(mActivity.findViewById(android.R.id.content), + R.string.please_grant_permissions_for_external_storage, + Snackbar.LENGTH_INDEFINITE).setAction(R.string.activate, + new View.OnClickListener() { + @Override + public void onClick(View v) { + ActivityCompat.requestPermissions(mActivity, + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + mAction); + } + }).show(); + } else { + ActivityCompat.requestPermissions(mActivity, + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + mAction); + } + } + + @TargetApi(Build.VERSION_CODES.M) + public static void requestBatteryPermmssions(FragmentActivity activity, Context context) { + final Context mContext = context; + final String packageName = mContext.getPackageName(); + PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + + if (pm.isIgnoringBatteryOptimizations(packageName)) + return; + + Snackbar.make(activity.findViewById(android.R.id.content), + R.string.consider_disable_battery_optimizations, + VERY_LONG_LENGTH).setAction(R.string.disable, + new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + intent.setData(Uri.parse("package:" + packageName)); + mContext.startActivity(intent); + } + }).show(); + } + + @TargetApi(Build.VERSION_CODES.M) + public static void requestDropBatteryPermmssions(FragmentActivity activity, Context context) { + final Context mContext = context; + + final String packageName = context.getPackageName(); + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + + if (!pm.isIgnoringBatteryOptimizations(packageName)) + return; + + Snackbar.make(activity.findViewById(android.R.id.content), + R.string.consider_enable_battery_optimizations, + VERY_LONG_LENGTH).setAction(R.string.enable, + new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS); + mContext.startActivity(intent); + } + }).show(); + } +} + diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/providers/CookieContentProvider.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/providers/CookieContentProvider.java new file mode 100644 index 00000000..f420a6d8 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/providers/CookieContentProvider.java @@ -0,0 +1,134 @@ +package org.torproject.android.ui.hiddenservices.providers; + +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.provider.BaseColumns; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.torproject.android.ui.hiddenservices.database.HSDatabase; + + +public class CookieContentProvider extends ContentProvider { + public static final String[] PROJECTION = new String[]{ + ClientCookie._ID, + ClientCookie.DOMAIN, + ClientCookie.AUTH_COOKIE_VALUE, + ClientCookie.ENABLED + }; + private static final String AUTH = "org.torproject.android.ui.hiddenservices.providers.cookie"; + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTH + "/cookie"); + //UriMatcher + private static final int COOKIES = 1; + private static final int COOKIE_ID = 2; + + private static final UriMatcher uriMatcher; + + static { + uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + uriMatcher.addURI(AUTH, "hs", COOKIES); + uriMatcher.addURI(AUTH, "hs/#", COOKIE_ID); + } + + private HSDatabase mServervices; + private Context mContext; + + @Override + public boolean onCreate() { + mContext = getContext(); + mServervices = new HSDatabase(mContext); + return true; + } + + @Nullable + @Override + public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + String where = selection; + if (uriMatcher.match(uri) == COOKIE_ID) { + where = "_id=" + uri.getLastPathSegment(); + } + + SQLiteDatabase db = mServervices.getReadableDatabase(); + + return db.query(HSDatabase.HS_CLIENT_COOKIE_TABLE_NAME, projection, where, + selectionArgs, null, null, sortOrder); + } + + @Nullable + @Override + public String getType(@NonNull Uri uri) { + int match = uriMatcher.match(uri); + + switch (match) { + case COOKIES: + return "vnd.android.cursor.dir/vnd.torproject.cookies"; + case COOKIE_ID: + return "vnd.android.cursor.item/vnd.torproject.cookie"; + default: + return null; + } + } + + @Nullable + @Override + public Uri insert(@NonNull Uri uri, ContentValues values) { + long regId; + + SQLiteDatabase db = mServervices.getWritableDatabase(); + + regId = db.insert(HSDatabase.HS_CLIENT_COOKIE_TABLE_NAME, null, values); + + mContext.getContentResolver().notifyChange(CONTENT_URI, null); + + return ContentUris.withAppendedId(CONTENT_URI, regId); + } + + @Override + public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { + + String where = selection; + if (uriMatcher.match(uri) == COOKIE_ID) { + where = "_id=" + uri.getLastPathSegment(); + } + + SQLiteDatabase db = mServervices.getWritableDatabase(); + + Integer rows = db.delete(HSDatabase.HS_CLIENT_COOKIE_TABLE_NAME, where, selectionArgs); + + mContext.getContentResolver().notifyChange(CONTENT_URI, null); + + return rows; + + } + + @Override + public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { + SQLiteDatabase db = mServervices.getWritableDatabase(); + + String where = selection; + if (uriMatcher.match(uri) == COOKIE_ID) { + where = "_id=" + uri.getLastPathSegment(); + } + + Integer rows = db.update(HSDatabase.HS_CLIENT_COOKIE_TABLE_NAME, values, where, null); + mContext.getContentResolver().notifyChange(CONTENT_URI, null); + + return rows; + } + + public static final class ClientCookie implements BaseColumns { + public static final String DOMAIN = "domain"; + public static final String AUTH_COOKIE_VALUE = "auth_cookie_value"; + public static final String ENABLED = "enabled"; + + private ClientCookie() { + } + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/providers/HSContentProvider.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/providers/HSContentProvider.java new file mode 100644 index 00000000..d7b558fd --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/providers/HSContentProvider.java @@ -0,0 +1,144 @@ +package org.torproject.android.ui.hiddenservices.providers; + +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.provider.BaseColumns; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.torproject.android.ui.hiddenservices.database.HSDatabase; + + +public class HSContentProvider extends ContentProvider { + public static final String[] PROJECTION = new String[]{ + HiddenService._ID, + HiddenService.NAME, + HiddenService.PORT, + HiddenService.DOMAIN, + HiddenService.ONION_PORT, + HiddenService.AUTH_COOKIE, + HiddenService.AUTH_COOKIE_VALUE, + HiddenService.CREATED_BY_USER, + HiddenService.ENABLED + }; + private static final String AUTH = "org.torproject.android.ui.hiddenservices.providers"; + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTH + "/hs"); + //UriMatcher + private static final int ONIONS = 1; + private static final int ONION_ID = 2; + + private static final UriMatcher uriMatcher; + + static { + uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + uriMatcher.addURI(AUTH, "hs", ONIONS); + uriMatcher.addURI(AUTH, "hs/#", ONION_ID); + } + + private HSDatabase mServervices; + private Context mContext; + + @Override + public boolean onCreate() { + mContext = getContext(); + mServervices = new HSDatabase(mContext); + return true; + } + + @Nullable + @Override + public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + String where = selection; + if (uriMatcher.match(uri) == ONION_ID) { + where = "_id=" + uri.getLastPathSegment(); + } + + SQLiteDatabase db = mServervices.getReadableDatabase(); + + return db.query(HSDatabase.HS_DATA_TABLE_NAME, projection, where, + selectionArgs, null, null, sortOrder); + } + + @Nullable + @Override + public String getType(@NonNull Uri uri) { + int match = uriMatcher.match(uri); + + switch (match) { + case ONIONS: + return "vnd.android.cursor.dir/vnd.torproject.onions"; + case ONION_ID: + return "vnd.android.cursor.item/vnd.torproject.onion"; + default: + return null; + } + } + + @Nullable + @Override + public Uri insert(@NonNull Uri uri, ContentValues values) { + long regId; + + SQLiteDatabase db = mServervices.getWritableDatabase(); + + regId = db.insert(HSDatabase.HS_DATA_TABLE_NAME, null, values); + + mContext.getContentResolver().notifyChange(CONTENT_URI, null); + + return ContentUris.withAppendedId(CONTENT_URI, regId); + } + + @Override + public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { + + String where = selection; + if (uriMatcher.match(uri) == ONION_ID) { + where = "_id=" + uri.getLastPathSegment(); + } + + SQLiteDatabase db = mServervices.getWritableDatabase(); + + Integer rows = db.delete(HSDatabase.HS_DATA_TABLE_NAME, where, selectionArgs); + + mContext.getContentResolver().notifyChange(CONTENT_URI, null); + + return rows; + + } + + @Override + public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { + SQLiteDatabase db = mServervices.getWritableDatabase(); + + String where = selection; + if (uriMatcher.match(uri) == ONION_ID) { + where = "_id=" + uri.getLastPathSegment(); + } + + Integer rows = db.update(HSDatabase.HS_DATA_TABLE_NAME, values, where, null); + mContext.getContentResolver().notifyChange(CONTENT_URI, null); + + return rows; + } + + public static final class HiddenService implements BaseColumns { + public static final String NAME = "name"; + public static final String PORT = "port"; + public static final String ONION_PORT = "onion_port"; + public static final String DOMAIN = "domain"; + public static final String AUTH_COOKIE = "auth_cookie"; + public static final String AUTH_COOKIE_VALUE = "auth_cookie_value"; + public static final String CREATED_BY_USER = "created_by_user"; + public static final String ENABLED = "enabled"; + + private HiddenService() { + } + } +} diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/storage/ExternalStorage.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/storage/ExternalStorage.java new file mode 100644 index 00000000..c50108b7 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/storage/ExternalStorage.java @@ -0,0 +1,34 @@ +package org.torproject.android.ui.hiddenservices.storage; + +import android.os.Environment; + +import java.io.File; + +public class ExternalStorage { + private static final String ORBOT_BACKUPS_DIR = "Orbot"; + + public static File getOrCreateBackupDir() { + if (!isExternalStorageWritable()) + return null; + + File dir = new File(Environment.getExternalStorageDirectory(), ORBOT_BACKUPS_DIR); + + if (!dir.isDirectory() && !dir.mkdirs()) + return null; + + return dir; + } + + /* Checks if external storage is available for read and write */ + public static boolean isExternalStorageWritable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state); + } + + /* Checks if external storage is available to at least read */ + public static boolean isExternalStorageReadable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state) || + Environment.MEDIA_MOUNTED_READ_ONLY.equals(state); + } +} diff --git a/app/src/main/res/layout/layout_activity_client_cookies.xml b/app/src/main/res/layout/layout_activity_client_cookies.xml new file mode 100644 index 00000000..b8f0626e --- /dev/null +++ b/app/src/main/res/layout/layout_activity_client_cookies.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_add_client_cookie_dialog.xml b/app/src/main/res/layout/layout_add_client_cookie_dialog.xml new file mode 100644 index 00000000..4669a921 --- /dev/null +++ b/app/src/main/res/layout/layout_add_client_cookie_dialog.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + +