Compare commits
17 Commits
v0.0-echob
...
master
Author | SHA1 | Date |
---|---|---|
|
082beb5691 | |
|
66a25b0efa | |
|
2da20a5962 | |
|
45d56ae601 | |
|
9b1e768226 | |
|
14fc595760 | |
|
fd5b1826de | |
|
63b4a86999 | |
|
3ee025fb23 | |
|
00b8ff4fc3 | |
|
9a1d8df640 | |
|
fc279e8062 | |
|
1e48f69691 | |
|
6baa48596b | |
|
3da1cc5032 | |
|
c68a45f088 | |
|
767fccef40 |
|
@ -1,5 +1,6 @@
|
||||||
*.iml
|
*.iml
|
||||||
*.aar
|
*.aar
|
||||||
|
*.apk
|
||||||
.gradle
|
.gradle
|
||||||
/local.properties
|
/local.properties
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
|
|
10
README.md
10
README.md
|
@ -4,3 +4,13 @@ A simple proof of concept usage of Ricochet + Tor on mobile
|
||||||
Uses:
|
Uses:
|
||||||
- Orbot
|
- Orbot
|
||||||
- gomobile: to leverage the go ricochet protocol library
|
- gomobile: to leverage the go ricochet protocol library
|
||||||
|
|
||||||
|
## Orbot setup
|
||||||
|
|
||||||
|
In Orbot's Settings, find at the bottom `Debug / Torrc Custom Config` and input:
|
||||||
|
|
||||||
|
```
|
||||||
|
ControlPort 9051
|
||||||
|
CookieAuthentication 0
|
||||||
|
DisableNetwork 0
|
||||||
|
```
|
||||||
|
|
|
@ -8,7 +8,7 @@ android {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 25
|
targetSdkVersion 25
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "0.9.0"
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -24,9 +24,10 @@ dependencies {
|
||||||
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
|
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
|
||||||
exclude group: 'com.android.support', module: 'support-annotations'
|
exclude group: 'com.android.support', module: 'support-annotations'
|
||||||
})
|
})
|
||||||
|
compile project(':goRicochetMobile')
|
||||||
|
compile project(':goRicochetMobileOd')
|
||||||
|
|
||||||
compile 'com.android.support:appcompat-v7:25.3.1'
|
compile 'com.android.support:appcompat-v7:25.3.1'
|
||||||
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||||
compile project(':goRicochetMobile')
|
|
||||||
|
|
||||||
testCompile 'junit:junit:4.12'
|
testCompile 'junit:junit:4.12'
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,14 +12,14 @@
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
<activity android:name=".ConnectActivity">
|
||||||
<activity android:name="im.ricochet.androidod.ConnectActivity">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity android:name=".RemoteActivity"></activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -1,66 +1,223 @@
|
||||||
package im.ricochet.androidod;
|
package im.ricochet.androidod;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.StrictMode;
|
import android.os.StrictMode;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.net.Socket;
|
|
||||||
|
|
||||||
import goRicochetMobile.GoRicochetMobile;
|
import goRicochetMobile.GoRicochetMobile;
|
||||||
|
import od.Od;
|
||||||
|
|
||||||
|
|
||||||
public class ConnectActivity extends AppCompatActivity {
|
public class ConnectActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private static final int ONION_ADDRESS_LENGTH = 16;
|
||||||
public static final String PREFERNCE_FILE = "im.ricochet.PREFERENCE_FILE";
|
public static final String PREFERNCE_FILE = "im.ricochet.PREFERENCE_FILE";
|
||||||
private static final String TAG = "InitActivity";
|
private static final String TAG = "InitActivity";
|
||||||
private static final String PRIVATE_KEY_KEY = "privateKey";
|
private static final String PRIVATE_KEY_KEY = "privateKey";
|
||||||
|
private static final String CONNECT_TO_ADDRESS_KEY = "connectToAddress";
|
||||||
|
private static final String RICOCHET_ADDRESS_PREFIX = "ricochet:";
|
||||||
|
|
||||||
|
Button regenButton;
|
||||||
|
Button connectButton;
|
||||||
|
ProgressBar connectSpinner;
|
||||||
|
ProgressBar regenIdentSpinner;
|
||||||
|
TextView connectStatusText;
|
||||||
|
TextView regenIdentStatusText;
|
||||||
|
TextView idetityText;
|
||||||
|
EditText addressText;
|
||||||
|
|
||||||
|
SharedPreferences prefs;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(im.ricochet.androidod.R.layout.activity_connect);
|
setContentView(im.ricochet.androidod.R.layout.activity_connect);
|
||||||
|
|
||||||
TextView progressText = (TextView)findViewById(im.ricochet.androidod.R.id.progressTextView);
|
connectStatusText = (TextView)findViewById(R.id.connectStatusTextView);
|
||||||
progressText.setText("Loading private key...");
|
regenIdentStatusText = (TextView)findViewById(R.id.regenIdentStatusText);
|
||||||
|
idetityText = (TextView)findViewById(R.id.identityTextView);
|
||||||
|
regenButton = (Button)findViewById(R.id.regenIdentButton);
|
||||||
|
connectButton = (Button)findViewById(R.id.connectButton);
|
||||||
|
connectSpinner = (ProgressBar)findViewById(R.id.connectProgressBar);
|
||||||
|
regenIdentSpinner = (ProgressBar)findViewById(R.id.regenIdentProgressBar);
|
||||||
|
addressText = (EditText)findViewById(R.id.addressText);
|
||||||
|
|
||||||
|
prefs = getSharedPreferences(PREFERNCE_FILE, MODE_PRIVATE);
|
||||||
|
|
||||||
|
regenButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
public void onClick(View v) {
|
||||||
|
RegenIdentTask regenIdentTask = new RegenIdentTask();
|
||||||
|
regenIdentTask.execute();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connectButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
public void onClick(View v) {
|
||||||
|
ConnectTask connectTask = new ConnectTask();
|
||||||
|
String connectToAddr = addressText.getText().toString().replaceFirst(RICOCHET_ADDRESS_PREFIX, "");
|
||||||
|
String privateKey = prefs.getString(PRIVATE_KEY_KEY, "");
|
||||||
|
connectTask.execute(privateKey, connectToAddr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addressText.addTextChangedListener(new TextWatcher(){
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||||
|
validateOnionAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Log.i(TAG, "Loading private key...");
|
|
||||||
SharedPreferences prefs = getSharedPreferences(PREFERNCE_FILE, MODE_PRIVATE);
|
|
||||||
String privateKey = prefs.getString(PRIVATE_KEY_KEY, "");
|
String privateKey = prefs.getString(PRIVATE_KEY_KEY, "");
|
||||||
Log.i(TAG, "Loaded! '" + privateKey + "'");
|
Log.i(TAG, "Private key loaded:\n"+privateKey);
|
||||||
if (privateKey.equals("")) {
|
if (privateKey.equals("")) {
|
||||||
Log.i(TAG, "Failed to load private key from preferences, generating new");
|
Log.i(TAG, "Generating pricate key");
|
||||||
progressText.setText("Generating private key...");
|
RegenIdentTask regenIdentTask = new RegenIdentTask();
|
||||||
|
regenIdentTask.execute();
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Setting identity, Enabling regen");
|
||||||
|
setIdentity(privateKey);
|
||||||
|
Button regenButton = (Button)findViewById(R.id.regenIdentButton);
|
||||||
|
regenButton.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
String connectToAddress = prefs.getString(CONNECT_TO_ADDRESS_KEY, RICOCHET_ADDRESS_PREFIX);
|
||||||
|
addressText.setText(connectToAddress);
|
||||||
|
validateOnionAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ConnectTask extends AsyncTask<String, Void, String> {
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute() {
|
||||||
|
Log.i(TAG, "ConnectTask.onPreExecute()");
|
||||||
|
regenButton.setEnabled(false);
|
||||||
|
connectButton.setEnabled(false);
|
||||||
|
connectStatusText.setText("Connecting to Onion server...");
|
||||||
|
connectSpinner.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
// execute(privateKey, connectToAddr)
|
||||||
|
// return string of exception if failed
|
||||||
|
public String doInBackground(String... params) {
|
||||||
|
Log.i(TAG, "ConntectTask.doInBackground()");
|
||||||
|
String privateKey = params[0];
|
||||||
|
String connectToAddr = params[1].replaceFirst(RICOCHET_ADDRESS_PREFIX, "");
|
||||||
try {
|
try {
|
||||||
privateKey = GoRicochetMobile.generatePrivateKey();
|
Od.odClientConnect(privateKey, connectToAddr);
|
||||||
|
return "";
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, e.toString());
|
Log.e(TAG, e.toString());
|
||||||
|
return e.toString();
|
||||||
}
|
}
|
||||||
Log.i(TAG, "Generated!: '" + privateKey + "'");
|
|
||||||
SharedPreferences.Editor prefsEditor = prefs.edit();
|
|
||||||
prefsEditor.putString(PRIVATE_KEY_KEY, privateKey);
|
|
||||||
prefsEditor.commit();
|
|
||||||
}
|
}
|
||||||
Log.i(TAG, "Ready!");
|
|
||||||
progressText.setText("Ready!");
|
|
||||||
ProgressBar progressBar = (ProgressBar)findViewById(im.ricochet.androidod.R.id.progressBar);
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(String exception) {
|
||||||
|
Log.i(TAG, "ConnectTask.onPostExecute(): " );
|
||||||
|
connectSpinner.setVisibility(View.INVISIBLE);
|
||||||
|
if (exception != "") {
|
||||||
|
connectStatusText.setText("ERROR connecting: " + exception);
|
||||||
|
connectButton.setEnabled(true);
|
||||||
|
regenButton.setEnabled(true);
|
||||||
|
} else {
|
||||||
|
connectStatusText.setText("Connected!");
|
||||||
|
Intent i = new Intent(getApplicationContext(), RemoteActivity.class);
|
||||||
|
// i,putExtra("key", "val")
|
||||||
|
startActivity(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setIdentity(String privateKey) {
|
||||||
|
Log.i(TAG, "setIdentity()");
|
||||||
|
String addr = GoRicochetMobile.getOnionAddress(privateKey);
|
||||||
|
Log.i(TAG, "setIdentity(): addr: '" + addr + "'");
|
||||||
|
idetityText.setText("ricochet:" + addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateOnionAddress() {
|
||||||
|
String address = addressText.getText().toString();
|
||||||
|
if (address.startsWith(RICOCHET_ADDRESS_PREFIX) && address.length() == ONION_ADDRESS_LENGTH + RICOCHET_ADDRESS_PREFIX.length()) {
|
||||||
|
SharedPreferences.Editor prefsEditor = prefs.edit();
|
||||||
|
prefsEditor.putString(CONNECT_TO_ADDRESS_KEY, address);
|
||||||
|
prefsEditor.commit();
|
||||||
|
connectButton.setEnabled(true);
|
||||||
|
} else {
|
||||||
|
connectButton.setEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RegenIdentTask extends AsyncTask<Void, Void, String> {
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute() {
|
||||||
|
Log.i(TAG, "RegenIdentTask.onPreExecute()");
|
||||||
|
regenButton.setEnabled(false);
|
||||||
|
connectButton.setEnabled(false);
|
||||||
|
regenIdentStatusText.setText("Generating new identity...");
|
||||||
|
regenIdentSpinner.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String doInBackground(Void... voids) {
|
||||||
|
Log.i(TAG, "RegenIdentTask.doInBackground()");
|
||||||
|
String privateKey;
|
||||||
|
try {
|
||||||
|
privateKey = GoRicochetMobile.generatePrivateKey();
|
||||||
|
|
||||||
|
SharedPreferences.Editor prefsEditor = prefs.edit();
|
||||||
|
prefsEditor.putString(PRIVATE_KEY_KEY, privateKey);
|
||||||
|
prefsEditor.commit();
|
||||||
|
return privateKey;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, e.toString());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(String privateKey) {
|
||||||
|
Log.i(TAG, "RegenIdentTask.onPostExecute(): " + privateKey);
|
||||||
|
regenIdentSpinner.setVisibility(View.INVISIBLE);
|
||||||
|
regenButton.setEnabled(true);
|
||||||
|
validateOnionAddress();
|
||||||
|
if (privateKey == null) {
|
||||||
|
regenIdentStatusText.setText("ERROR: unable to generate new identity");
|
||||||
|
} else {
|
||||||
|
regenIdentStatusText.setText("");
|
||||||
|
setIdentity(privateKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/********** TESTING by standing up an echobot ************/
|
||||||
|
|
||||||
|
private void echoBot(String privateKey) {
|
||||||
|
// Test echobot
|
||||||
Log.i(TAG, "Setting thread policy perms");
|
Log.i(TAG, "Setting thread policy perms");
|
||||||
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
|
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
|
||||||
StrictMode.setThreadPolicy(policy);
|
StrictMode.setThreadPolicy(policy);
|
||||||
|
|
||||||
Log.i(TAG, "Starting Echo Bot");
|
Log.i(TAG, "Starting Echo Bot");
|
||||||
//GoRicochetMobile.echoBot(privateKey);
|
|
||||||
new EchoBot(privateKey).execute();
|
new EchoBot(privateKey).execute();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class EchoBot extends AsyncTask<Void, Void, Void> {
|
private class EchoBot extends AsyncTask<Void, Void, Void> {
|
||||||
|
|
|
@ -0,0 +1,184 @@
|
||||||
|
package im.ricochet.androidod;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import od.Od;
|
||||||
|
|
||||||
|
public class RemoteActivity extends AppCompatActivity {
|
||||||
|
private static final String DEVICE_NAME_KEY = "deviceName";
|
||||||
|
private static final String BATTERY_LEVEL_KEY = "batteryLevel";
|
||||||
|
private static final String VIBE_LEVEL_KEY = "vibeLEvel";
|
||||||
|
|
||||||
|
TextView deviceText;
|
||||||
|
TextView batteryText;
|
||||||
|
TextView statusText;
|
||||||
|
|
||||||
|
Button offButton;
|
||||||
|
Button lowButton;
|
||||||
|
Button medButton;
|
||||||
|
Button highButton;
|
||||||
|
Button disconnectButton;
|
||||||
|
|
||||||
|
int vibeLevel = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_remote);
|
||||||
|
|
||||||
|
deviceText = (TextView)findViewById(R.id.deviceName);
|
||||||
|
batteryText = (TextView)findViewById(R.id.batteryLevel);
|
||||||
|
statusText = (TextView)findViewById(R.id.status);
|
||||||
|
|
||||||
|
offButton = (Button)findViewById(R.id.offButton);
|
||||||
|
lowButton = (Button)findViewById(R.id.lowButton);
|
||||||
|
medButton = (Button)findViewById(R.id.medButton);
|
||||||
|
highButton = (Button)findViewById(R.id.highButton);
|
||||||
|
disconnectButton = (Button)findViewById(R.id.disconnetButton);
|
||||||
|
|
||||||
|
offButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
public void onClick(View v) {
|
||||||
|
SetLevelTask setLevelTask = new SetLevelTask();
|
||||||
|
setLevelTask.execute(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
lowButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
public void onClick(View v) {
|
||||||
|
SetLevelTask setLevelTask = new SetLevelTask();
|
||||||
|
setLevelTask.execute(2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
medButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
public void onClick(View v) {
|
||||||
|
SetLevelTask setLevelTask = new SetLevelTask();
|
||||||
|
setLevelTask.execute(4);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
highButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
public void onClick(View v) {
|
||||||
|
SetLevelTask setLevelTask = new SetLevelTask();
|
||||||
|
setLevelTask.execute(6);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
disconnectButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
public void onClick(View v) {
|
||||||
|
DisconnectTask disconnectTask = new DisconnectTask();
|
||||||
|
disconnectTask.execute();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
deviceText.setText(savedInstanceState.getString(DEVICE_NAME_KEY));
|
||||||
|
batteryText.setText(savedInstanceState.getString(BATTERY_LEVEL_KEY));
|
||||||
|
vibeLevel = savedInstanceState.getInt(VIBE_LEVEL_KEY);
|
||||||
|
setVibeLevel(vibeLevel);
|
||||||
|
} else {
|
||||||
|
GetDeviceNameTask getDeviceNameTask = new GetDeviceNameTask();
|
||||||
|
getDeviceNameTask.execute();
|
||||||
|
|
||||||
|
GetBatteryTask getBatteryTask = new GetBatteryTask();
|
||||||
|
getBatteryTask.execute();
|
||||||
|
|
||||||
|
GetLevelTask getLevelTask = new GetLevelTask();
|
||||||
|
getLevelTask.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSaveInstanceState(final Bundle outState) {
|
||||||
|
outState.putString(DEVICE_NAME_KEY, deviceText.getText().toString());
|
||||||
|
outState.putString(BATTERY_LEVEL_KEY, batteryText.getText().toString());
|
||||||
|
outState.putInt(VIBE_LEVEL_KEY, vibeLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// current impelementation of OD server can return 0 to 6 (potentially downsampled from 12?)
|
||||||
|
// The go API respects that, further downsample to [off low med high] here
|
||||||
|
// Assuming 1 means off and 2-6 are active states...
|
||||||
|
private void setVibeLevel(int newVibeLevel) {
|
||||||
|
vibeLevel = newVibeLevel;
|
||||||
|
if (vibeLevel == 1) {
|
||||||
|
statusText.setText("Off");
|
||||||
|
} else if (vibeLevel == 2) {
|
||||||
|
statusText.setText("Low");
|
||||||
|
} else if (vibeLevel == 3 || vibeLevel == 4) {
|
||||||
|
statusText.setText("Medium");
|
||||||
|
} else if (vibeLevel == 5 || vibeLevel == 6) {
|
||||||
|
statusText.setText("High");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GetDeviceNameTask extends AsyncTask<Void, Void, String> {
|
||||||
|
@Override
|
||||||
|
protected String doInBackground(Void... params) {
|
||||||
|
String name = Od.getDeviceName();
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(String deviceName) {
|
||||||
|
deviceText.setText(deviceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GetBatteryTask extends AsyncTask<Void, Void, String> {
|
||||||
|
@Override
|
||||||
|
protected String doInBackground(Void... params) {
|
||||||
|
String batteryLevel = Od.getBatteryLevel();
|
||||||
|
return batteryLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(String batteryLevel) {
|
||||||
|
batteryText.setText(batteryLevel + "%");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GetLevelTask extends AsyncTask<Void, Void, Integer> {
|
||||||
|
@Override
|
||||||
|
protected Integer doInBackground(Void... params) {
|
||||||
|
Integer vibeLevel = (int) Od.getVibeLevel();
|
||||||
|
return vibeLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Integer newVibeLevel) {
|
||||||
|
setVibeLevel(newVibeLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SetLevelTask extends AsyncTask<Integer, Void, Integer> {
|
||||||
|
@Override
|
||||||
|
protected Integer doInBackground(Integer... params) {
|
||||||
|
Integer newVibeLevel = params[0];
|
||||||
|
Od.setVibeLevel(newVibeLevel);
|
||||||
|
return newVibeLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Integer newVibeLevel) {
|
||||||
|
setVibeLevel(newVibeLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DisconnectTask extends AsyncTask<Void, Void, Void> {
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(Void ...params) {
|
||||||
|
Od.odClientDisconnect();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Void nothing) {
|
||||||
|
Intent i = new Intent(getApplicationContext(), ConnectActivity.class);
|
||||||
|
startActivity(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,64 +7,72 @@
|
||||||
tools:context="im.ricochet.androidod.im.ricochet.androidod.ConnectActivity">
|
tools:context="im.ricochet.androidod.im.ricochet.androidod.ConnectActivity">
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/connectProgressBar"
|
||||||
style="@style/Widget.AppCompat.ProgressBar"
|
style="@style/Widget.AppCompat.ProgressBar"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:layout_marginRight="8dp"
|
android:layout_marginTop="7dp"
|
||||||
android:layout_marginTop="32dp"
|
android:visibility="invisible"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toRightOf="@+id/connectButton"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintTop_toBottomOf="@+id/addressText"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/connect" />
|
android:layout_marginStart="8dp" />
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/addressText"
|
android:id="@+id/addressText"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="241dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="42dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:layout_marginRight="8dp"
|
android:layout_marginRight="8dp"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="9dp"
|
||||||
android:ems="10"
|
android:ems="10"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
|
android:minEms="26"
|
||||||
android:text="ricochet:"
|
android:text="ricochet:"
|
||||||
app:layout_constraintHorizontal_bias="0.503"
|
app:layout_constraintHorizontal_bias="0.518"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/textView" />
|
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||||
|
tools:layout_editor_absoluteX="50dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView"
|
android:id="@+id/textView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Onion Address:"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginLeft="0dp"
|
android:text="Connect to Onion address:"
|
||||||
app:layout_constraintLeft_toLeftOf="@+id/addressText" />
|
app:layout_constraintLeft_toLeftOf="@+id/addressText"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/connect"
|
android:id="@+id/connectButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:layout_marginRight="8dp"
|
android:layout_marginRight="8dp"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="7dp"
|
||||||
|
android:enabled="false"
|
||||||
android:text="Connect"
|
android:text="Connect"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/addressText" />
|
app:layout_constraintTop_toBottomOf="@+id/addressText"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/progressTextView"
|
android:id="@+id/connectStatusTextView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
android:layout_marginRight="8dp"
|
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/connectButton"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/progressBar" />
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView2"
|
android:id="@+id/textView2"
|
||||||
|
@ -72,11 +80,14 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:layout_marginRight="8dp"
|
android:layout_marginRight="8dp"
|
||||||
android:layout_marginTop="32dp"
|
|
||||||
android:text="Required step: Orbot/Settings/Torrc Custom Config:"
|
android:text="Required step: Orbot/Settings/Torrc Custom Config:"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/progressTextView" />
|
app:layout_constraintHorizontal_bias="0.511"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/connectStatusTextView"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView3"
|
android:id="@+id/textView3"
|
||||||
|
@ -88,7 +99,9 @@
|
||||||
android:layout_marginRight="8dp"
|
android:layout_marginRight="8dp"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
app:layout_constraintLeft_toLeftOf="parent" />
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView4"
|
android:id="@+id/textView4"
|
||||||
|
@ -97,6 +110,71 @@
|
||||||
android:text="CookieAuthentication 0"
|
android:text="CookieAuthentication 0"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/textView3"
|
app:layout_constraintTop_toBottomOf="@+id/textView3"
|
||||||
android:layout_marginLeft="0dp"
|
|
||||||
app:layout_constraintLeft_toLeftOf="@+id/textView3" />
|
app:layout_constraintLeft_toLeftOf="@+id/textView3" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView5"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="DisableNetwork 0"
|
||||||
|
app:layout_constraintLeft_toLeftOf="@+id/textView4"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/textView4" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView6"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="32dp"
|
||||||
|
android:text="Identity:"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/textView5"
|
||||||
|
app:layout_constraintLeft_toLeftOf="@+id/regenIdentButton" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/identityTextView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="ricochet:"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/textView6"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/textView6"
|
||||||
|
android:layout_marginStart="8dp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/regenIdentButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:enabled="false"
|
||||||
|
android:text="New Identity"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/textView6"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/regenIdentProgressBar"
|
||||||
|
style="?android:attr/progressBarStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginTop="0dp"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/regenIdentButton"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/regenIdentButton" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/regenIdentStatusText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/regenIdentButton"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
</android.support.constraint.ConstraintLayout>
|
</android.support.constraint.ConstraintLayout>
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context="im.ricochet.androidod.RemoteActivity">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView7"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Device:"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/deviceName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/textView7"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/textView7"
|
||||||
|
android:layout_marginTop="0dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView9"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="Battery:"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/textView7" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/batteryLevel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:text="0%"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/textView9"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/textView9"
|
||||||
|
android:layout_marginTop="0dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/TextView10"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="Status:"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/textView9" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/TextView10"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/TextView10"
|
||||||
|
android:layout_marginTop="0dp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/offButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="Off"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/TextView10"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/lowButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:text="Low"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/offButton"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/medButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="Medium"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/lowButton" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/highButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="High"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/medButton" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/disconnetButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginTop="64dp"
|
||||||
|
android:text="Disconnect"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/highButton" />
|
||||||
|
</android.support.constraint.ConstraintLayout>
|
|
@ -8,4 +8,5 @@ gobind {
|
||||||
GOMOBILE = "/home/user/src/go/bin/gomobile"
|
GOMOBILE = "/home/user/src/go/bin/gomobile"
|
||||||
GOBIND = "/home/user/src/go/bin/gobind"
|
GOBIND = "/home/user/src/go/bin/gobind"
|
||||||
GO = "/home/user/go/bin/go"
|
GO = "/home/user/go/bin/go"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
plugins {
|
||||||
|
id "org.golang.mobile.bind" version "0.2.7"
|
||||||
|
}
|
||||||
|
|
||||||
|
gobind {
|
||||||
|
pkg = "github.com/dballard/goRicochetMobile/od"
|
||||||
|
GOPATH = "/home/user/src/go"
|
||||||
|
GOMOBILE = "/home/user/src/go/bin/gomobile"
|
||||||
|
GOBIND = "/home/user/src/go/bin/gobind"
|
||||||
|
GO = "/home/user/go/bin/go"
|
||||||
|
}
|
|
@ -1 +1 @@
|
||||||
include ':app', 'goRicochetMobile'
|
include ':app', 'goRicochetMobile', 'goRicochetMobileOd'
|
||||||
|
|
Loading…
Reference in New Issue