Skip to content

Commit 9149661

Browse files
committed
Merge branch 'survey_api' into dev
2 parents 178f74b + 55ca1ac commit 9149661

24 files changed

+2942
-11
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ Feel free to open an Issue for ideas, bugs, localisation or feature requests. Or
2525
## FAQ
2626
- Docs ?!
2727
[Docs!](https://github.com/0xpr03/VocableTrainer-Android/wiki)
28+
- What about these internet permissions?!
29+
These are used only the take part in an opt-in android-version survey.
2830
- What about a localisation for X ?
2931
I would appreciate a PR for localizations.
3032
- What is this first `1.0` version before `0.1` about ?

app/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,6 @@ dependencies {
4646
implementation 'androidx.constraintlayout:constraintlayout:1.1.2'
4747
implementation "com.google.android.material:material:$androidSupportVersion"
4848
testImplementation 'junit:junit:4.12'
49+
50+
implementation "info.guardianproject.netcipher:netcipher:2.0.0-alpha1"
4951
}

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
77
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
8+
<uses-permission android:name="android.permission.INTERNET" />
89

910
<application
1011
android:allowBackup="true"

app/src/main/java/vocabletrainer/heinecke/aron/vocabletrainer/activity/MainActivity.java

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
import android.content.Intent;
55
import android.content.SharedPreferences;
66
import android.os.Bundle;
7-
import androidx.appcompat.app.AlertDialog;
8-
import androidx.appcompat.app.AppCompatActivity;
97
import android.view.View;
108
import android.widget.Button;
119

10+
import androidx.appcompat.app.AlertDialog;
11+
import androidx.appcompat.app.AppCompatActivity;
1212
import vocabletrainer.heinecke.aron.vocabletrainer.R;
13+
import vocabletrainer.heinecke.aron.vocabletrainer.dialog.SurveyDialog;
1314
import vocabletrainer.heinecke.aron.vocabletrainer.lib.Database;
1415
import vocabletrainer.heinecke.aron.vocabletrainer.lib.Widget.VectorImageHelper;
1516

@@ -27,6 +28,7 @@ public class MainActivity extends AppCompatActivity {
2728
private static final String P_KEY_DB_CHANGE_N_N = "showedDBDialogN_N";
2829
private static boolean showedDialog = false;
2930
Button btnContinue;
31+
SurveyDialog surveyDialog;
3032

3133
@Override
3234
protected void onCreate(Bundle savedInstanceState) {
@@ -48,10 +50,17 @@ protected void onCreate(Bundle savedInstanceState) {
4850
betaWarnDiag.show();
4951
}
5052

53+
if (savedInstanceState != null) {
54+
surveyDialog = (SurveyDialog) getSupportFragmentManager().getFragment(savedInstanceState, SurveyDialog.TAG);
55+
}
56+
if (surveyDialog == null && !SurveyDialog.wasSurveyDisplayed(this)) {
57+
surveyDialog = SurveyDialog.newInstance();
58+
surveyDialog.show(getSupportFragmentManager(), SurveyDialog.TAG);
59+
}
5160

5261
btnContinue = findViewById(R.id.bLastSession);
53-
VectorImageHelper helper = new VectorImageHelper(this,findViewById(android.R.id.content));
54-
helper.initImageLeft(R.id.bLastSession,R.drawable.ic_play_arrow_white_24dp);
62+
VectorImageHelper helper = new VectorImageHelper(this, findViewById(android.R.id.content));
63+
helper.initImageLeft(R.id.bLastSession, R.drawable.ic_play_arrow_white_24dp);
5564
helper.initImageLeft(R.id.bTrainerEnter, R.drawable.ic_send_white_24dp);
5665
helper.initImageLeft(R.id.bEditTable, R.drawable.ic_edit_white_24dp);
5766
helper.initImageLeft(R.id.bAbout, R.drawable.ic_info_outline_white_24dp);
@@ -152,7 +161,7 @@ public void showExport(View view) {
152161
Intent myIntent = new Intent(this, PermActivity.class);
153162
myIntent.putExtra(PermActivity.PARAM_PERMISSION, ExImportActivity.REQUIRED_PERMISSION);
154163
myIntent.putExtra(PermActivity.PARAM_MESSAGE, getString(R.string.Perm_CSV));
155-
this.startActivityForResult(myIntent,REQUEST_PERM_EXPORT);
164+
this.startActivityForResult(myIntent, REQUEST_PERM_EXPORT);
156165
}
157166
}
158167

@@ -168,26 +177,32 @@ public void showImport(View view) {
168177
Intent myIntent = new Intent(this, PermActivity.class);
169178
myIntent.putExtra(PermActivity.PARAM_PERMISSION, ExImportActivity.REQUIRED_PERMISSION);
170179
myIntent.putExtra(PermActivity.PARAM_MESSAGE, getString(R.string.Perm_CSV));
171-
this.startActivityForResult(myIntent,REQUEST_PERM_IMPORT);
180+
this.startActivityForResult(myIntent, REQUEST_PERM_IMPORT);
172181
}
173182
}
174183

175184
/**
176185
* Start import activity, does not check for permissions
177186
*/
178-
private void startImportActivityUnchecked(){
187+
private void startImportActivityUnchecked() {
179188
Intent myIntent = new Intent(this, ExImportActivity.class);
180-
myIntent.putExtra(ExImportActivity.PARAM_IMPORT,true);
189+
myIntent.putExtra(ExImportActivity.PARAM_IMPORT, true);
181190
this.startActivity(myIntent);
182191
}
183192

184193
/**
185194
* Start export activity, does not check for permissions
186195
*/
187-
private void startExportActivityUnchecked(){
196+
private void startExportActivityUnchecked() {
188197
Intent myIntent = new Intent(this, ExImportActivity.class);
189-
myIntent.putExtra(ExImportActivity.PARAM_IMPORT,false);
198+
myIntent.putExtra(ExImportActivity.PARAM_IMPORT, false);
190199
this.startActivity(myIntent);
191200
}
192201

202+
@Override
203+
protected void onSaveInstanceState(Bundle outState) {
204+
super.onSaveInstanceState(outState);
205+
if (surveyDialog != null && surveyDialog.isAdded())
206+
getSupportFragmentManager().putFragment(outState, SurveyDialog.TAG, surveyDialog);
207+
}
193208
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package vocabletrainer.heinecke.aron.vocabletrainer.dialog;
2+
3+
import android.content.Context;
4+
import android.content.SharedPreferences;
5+
import android.os.Bundle;
6+
import android.text.method.LinkMovementMethod;
7+
import android.util.Log;
8+
import android.view.LayoutInflater;
9+
import android.view.View;
10+
import android.view.ViewGroup;
11+
import android.widget.Button;
12+
import android.widget.ProgressBar;
13+
import android.widget.TextView;
14+
import android.widget.Toast;
15+
16+
import androidx.annotation.NonNull;
17+
import androidx.annotation.Nullable;
18+
import androidx.fragment.app.DialogFragment;
19+
import androidx.lifecycle.ViewModelProviders;
20+
import vocabletrainer.heinecke.aron.vocabletrainer.R;
21+
import vocabletrainer.heinecke.aron.vocabletrainer.lib.ViewModel.SurveyViewModel;
22+
23+
import static vocabletrainer.heinecke.aron.vocabletrainer.activity.MainActivity.PREFS_NAME;
24+
25+
/**
26+
* Dialog showing progress during import/export and preview parsing.
27+
* Capable of two modes: indefinite mode, displaying the actual progress data and actual progress mode<br>
28+
* Works with a LiveData<Integer>
29+
* @author Aron Heinecke
30+
*/
31+
public class SurveyDialog extends DialogFragment {
32+
public static final String TAG = "SurveyDialog";
33+
private static final String P_KEY_SURVEY_DIALOG_API= "showedAPISurveyDialog";
34+
private ProgressBar progressBar;
35+
private Button btnCancel;
36+
private Button btnAccept;
37+
private SurveyViewModel surveyViewModel;
38+
private TextView msg;
39+
40+
@Override
41+
public void onCreate(@Nullable Bundle savedInstanceState) {
42+
super.onCreate(savedInstanceState);
43+
setStyle(DialogFragment.STYLE_NORMAL, R.style.CustomDialog);
44+
surveyViewModel = ViewModelProviders.of(getActivity()).get(SurveyViewModel.class);
45+
46+
surveyViewModel.getErrorLiveData().observe(this,err -> {
47+
if(err != null && err){
48+
Toast.makeText(getContext(),R.string.Survey_Diag_Error_Toast,Toast.LENGTH_LONG).show();
49+
50+
this.dismiss();
51+
}
52+
});
53+
54+
surveyViewModel.getLoadingLiveData().observe(this,loading -> {
55+
if(loading != null && loading) {
56+
btnAccept.setEnabled(false);
57+
btnCancel.setEnabled(false);
58+
progressBar.setVisibility(View.VISIBLE);
59+
} else if ( surveyViewModel.wasRunning() ) {
60+
progressBar.setVisibility(View.GONE);
61+
SharedPreferences settings = getContext().getSharedPreferences(PREFS_NAME, 0);
62+
settings.edit().putBoolean(P_KEY_SURVEY_DIALOG_API, true).apply();
63+
Toast.makeText(getContext(), R.string.Survey_Diag_Success_Toast, Toast.LENGTH_LONG).show();
64+
this.dismiss();
65+
}
66+
});
67+
}
68+
69+
@Override
70+
public void onSaveInstanceState(Bundle outState) {
71+
super.onSaveInstanceState(outState);
72+
}
73+
74+
@Override
75+
public void onResume() {
76+
super.onResume();
77+
this.setCancelable(false);
78+
}
79+
80+
public static boolean wasSurveyDisplayed(Context context) {
81+
SharedPreferences settings = context.getSharedPreferences(PREFS_NAME, 0);
82+
return settings.getBoolean(P_KEY_SURVEY_DIALOG_API,false);
83+
}
84+
85+
/**
86+
* Creates a new instance
87+
*/
88+
public static SurveyDialog newInstance(){
89+
SurveyDialog dialog = new SurveyDialog();
90+
Bundle args = new Bundle();
91+
dialog.setArguments(args);
92+
return dialog;
93+
}
94+
95+
@Nullable
96+
@Override
97+
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
98+
View view = View.inflate(getContext(),R.layout.dialog_survey, null);
99+
100+
progressBar = view.findViewById(R.id.dialog_progressbar);
101+
progressBar.setIndeterminate(true);
102+
progressBar.setVisibility(View.GONE);
103+
btnCancel = view.findViewById(R.id.button_decline_survey);
104+
btnAccept = view.findViewById(R.id.button_participate);
105+
msg = view.findViewById(R.id.survey_message);
106+
msg.setMovementMethod(LinkMovementMethod.getInstance());
107+
btnCancel.setOnClickListener(v -> {
108+
SharedPreferences settings = getContext().getSharedPreferences(PREFS_NAME, 0);
109+
settings.edit().putBoolean(P_KEY_SURVEY_DIALOG_API,true).apply();
110+
this.dismiss();
111+
});
112+
btnAccept.setOnClickListener(v -> {
113+
Log.d(TAG,"survey participating..");
114+
surveyViewModel.submitSurvey();
115+
});
116+
return view;
117+
}
118+
119+
120+
}

app/src/main/java/vocabletrainer/heinecke/aron/vocabletrainer/lib/StorageUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,12 @@ private static void retrieveStorageFilesystem(Map<String, File> storageLocations
168168
* @throws IllegalAccessException
169169
* @throws NoSuchMethodException
170170
*/
171+
@SuppressWarnings("unchecked")
171172
private static <T> T callReflectionFunction(Object obj, String function)
172173
throws ClassCastException,InvocationTargetException, IllegalAccessException, NoSuchMethodException {
173174
Method method = obj.getClass().getDeclaredMethod(function);
174175
method.setAccessible(true);
175176
Object r = method.invoke(obj);
176-
//noinspection unchecked
177177
return (T) r;
178178
}
179179
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package vocabletrainer.heinecke.aron.vocabletrainer.lib.ViewModel;
2+
3+
import android.os.Build;
4+
import android.util.Log;
5+
6+
import org.json.JSONException;
7+
import org.json.JSONObject;
8+
9+
import java.io.BufferedInputStream;
10+
import java.io.ByteArrayOutputStream;
11+
import java.io.IOException;
12+
import java.io.InputStream;
13+
import java.io.OutputStream;
14+
import java.net.MalformedURLException;
15+
import java.net.URL;
16+
17+
import javax.net.ssl.HttpsURLConnection;
18+
19+
import androidx.lifecycle.LiveData;
20+
import androidx.lifecycle.MutableLiveData;
21+
import androidx.lifecycle.ViewModel;
22+
import info.guardianproject.netcipher.NetCipher;
23+
24+
/**
25+
* ViewModel for survey, doing survey submit
26+
*/
27+
public class SurveyViewModel extends ViewModel {
28+
private static final String TAG = "SurveyViewModel";
29+
private MutableLiveData<Boolean> loading;
30+
private MutableLiveData<Boolean> error;
31+
private Thread runner;
32+
33+
public SurveyViewModel() {
34+
loading = new MutableLiveData<>();
35+
error = new MutableLiveData<>();
36+
loading.setValue(false);
37+
error.setValue(false);
38+
}
39+
40+
public synchronized void submitSurvey() {
41+
if (wasRunning()) {
42+
return;
43+
}
44+
loading.postValue(true);
45+
System.setProperty("http.keepAlive", "false");
46+
47+
final String url;
48+
// allow tls 1.0
49+
// can't be tested fully due to different device libraries
50+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
51+
url = "https://vct.badssl.proctet.net/data/add/api";
52+
} else {
53+
url = "https://vct.proctet.net/data/add/api";
54+
}
55+
56+
runner = new Thread(() -> {
57+
58+
final JSONObject jsonBody;
59+
try {
60+
HttpsURLConnection con = NetCipher.getHttpsURLConnection(new URL(url));
61+
con.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
62+
con.setRequestProperty("Accept", "application/json");
63+
con.setDoOutput(true);
64+
con.setDoInput(true);
65+
66+
jsonBody = new JSONObject("{\"api\":" + Build.VERSION.SDK_INT + "}");
67+
68+
OutputStream wr = con.getOutputStream();
69+
wr.write(jsonBody.toString().getBytes("UTF-8"));
70+
wr.flush();
71+
wr.close();
72+
73+
InputStream in = new BufferedInputStream(con.getInputStream());
74+
ByteArrayOutputStream result = new ByteArrayOutputStream();
75+
byte[] buffer = new byte[1024];
76+
int length;
77+
while ((length = in.read(buffer)) != -1) {
78+
result.write(buffer, 0, length);
79+
}
80+
81+
JSONObject jsonObject = new JSONObject(result.toString("UTF-8"));
82+
in.close();
83+
84+
con.disconnect();
85+
Log.d(TAG,"received json: "+jsonObject.toString());
86+
loading.postValue(false);
87+
} catch (
88+
JSONException e) {
89+
Log.wtf(TAG, e);
90+
error.postValue(true);
91+
} catch (MalformedURLException e) {
92+
Log.wtf(TAG, e);
93+
error.postValue(true);
94+
} catch (IOException e) {
95+
Log.e(TAG, "IOException:",e);
96+
error.postValue(true);
97+
}
98+
});
99+
runner.start();
100+
}
101+
102+
public boolean wasRunning() {
103+
return runner != null;
104+
}
105+
106+
public LiveData<Boolean> getErrorLiveData() {
107+
return this.error;
108+
}
109+
110+
public LiveData<Boolean> getLoadingLiveData() {
111+
return this.loading;
112+
}
113+
}

0 commit comments

Comments
 (0)