summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorcyberta <cyberta@riseup.net>2022-11-22 19:15:06 +0000
committercyberta <cyberta@riseup.net>2022-11-22 19:15:06 +0000
commitc3613c8521edb8f3e979331d2396977808581eeb (patch)
tree15aeeb09e1948466ae11b3591c7be89bb6de7bf9 /app
parent5bec8a2ab69cd3d457756ca43fa28b880f95befa (diff)
parent2d20c8e269a502945e981662abe1f7818090a618 (diff)
Merge branch 'motd' into 'master'
Motd Closes #9082 See merge request leap/bitmask_android!213
Diffstat (limited to 'app')
-rw-r--r--app/build.gradle4
-rw-r--r--app/src/custom/res/drawable/ic_motd.pngbin0 -> 34639 bytes
-rw-r--r--app/src/custom/res/drawable/motd_img.xml13
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java84
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java86
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/MotdFragment.java103
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java4
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java7
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java152
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java11
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java70
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java7
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/motd/MotdClient.java47
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java1
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java48
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java5
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CustomProviderSetupActivity.java5
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java1
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java1
-rw-r--r--app/src/main/res/drawable-hdpi/ic_arrow_right.pngbin0 -> 562 bytes
-rw-r--r--app/src/main/res/drawable-ldpi/ic_arrow_right.pngbin0 -> 458 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_arrow_right.pngbin0 -> 551 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_arrow_right.pngbin0 -> 753 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_arrow_right.pngbin0 -> 784 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/ic_arrow_right.pngbin0 -> 1168 bytes
-rw-r--r--app/src/main/res/drawable/background_motd.xml9
-rw-r--r--app/src/main/res/drawable/motd_img.xml18
-rw-r--r--app/src/main/res/layout/f_motd.xml110
-rw-r--r--app/src/main/res/values/colors.xml1
-rw-r--r--app/src/normal/assets/urls/riseup.net.url3
-rw-r--r--app/src/normalProductionFatDebug/assets/urls/riseup.net.url3
-rw-r--r--app/src/sharedTest/java/se.leap.bitmaskclient/testutils/TestSetupHelper.java1
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/base/utils/DateHelperTest.java20
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java34
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java3
-rw-r--r--app/src/test/resources/v4/riseup.net.cert96
36 files changed, 812 insertions, 135 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 239190fd..1e17f01d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -68,6 +68,7 @@ android {
buildConfigField "String", "customProviderIp", '""'
buildConfigField "String", "customProviderApiIp", '""'
buildConfigField "String", "geoipUrl", '""'
+ buildConfigField "String", "customProviderMotdUrl", '""'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
dexOptions {
@@ -133,6 +134,9 @@ android {
buildConfigField "String", "customProviderApiIp", customProviderApiIp
def geoipUrl = '"https://api.black.riseup.net:9001/json"'
buildConfigField "String", "geoipUrl", geoipUrl
+ //URL for the message of the day, see https://0xacab.org/leap/motd#motd-message-of-the-day
+ def customProviderMotdUrl = '"https://static.riseup.net/vpn/motd.json"'
+ buildConfigField "String", "customProviderMotdUrl", customProviderMotdUrl
//Change the versionCode as needed
//versionCode 1
//Change the versionName as needed
diff --git a/app/src/custom/res/drawable/ic_motd.png b/app/src/custom/res/drawable/ic_motd.png
new file mode 100644
index 00000000..5d2efea4
--- /dev/null
+++ b/app/src/custom/res/drawable/ic_motd.png
Binary files differ
diff --git a/app/src/custom/res/drawable/motd_img.xml b/app/src/custom/res/drawable/motd_img.xml
new file mode 100644
index 00000000..d27db1a0
--- /dev/null
+++ b/app/src/custom/res/drawable/motd_img.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+ <!--
+ *****************************************************
+ Used to create the image to be shown in the motd fragment
+ example: <item android:drawable="@drawable/mybackground" />
+ *****************************************************
+ -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_motd"
+ android:gravity="fill_horizontal|fill_vertical"/>
+</layer-list>
+
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java b/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java
index 8cb12652..e2fa0783 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java
@@ -17,6 +17,34 @@
package se.leap.bitmaskclient.base;
+import static se.leap.bitmaskclient.R.string.downloading_vpn_certificate_failed;
+import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message;
+import static se.leap.bitmaskclient.base.models.Constants.ASK_TO_CANCEL_VPN;
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_CODE;
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_LAUNCH_VPN;
+import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_PREPARE_VPN;
+import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START;
+import static se.leap.bitmaskclient.base.models.Constants.EIP_REQUEST;
+import static se.leap.bitmaskclient.base.models.Constants.EXTRA_MOTD_MSG;
+import static se.leap.bitmaskclient.base.models.Constants.LOCATION;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP;
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_LOG_IN;
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_SWITCH_PROVIDER;
+import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.storeProviderInPreferences;
+import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_VPN_PREPARE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORID;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.TOR_EXCEPTION;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.TOR_TIMEOUT;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.USER_MESSAGE;
+
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
@@ -40,6 +68,7 @@ import se.leap.bitmaskclient.base.fragments.EipFragment;
import se.leap.bitmaskclient.base.fragments.ExcludeAppsFragment;
import se.leap.bitmaskclient.base.fragments.LogFragment;
import se.leap.bitmaskclient.base.fragments.MainActivityErrorDialog;
+import se.leap.bitmaskclient.base.fragments.MotdFragment;
import se.leap.bitmaskclient.base.fragments.NavigationDrawerFragment;
import se.leap.bitmaskclient.base.fragments.SettingsFragment;
import se.leap.bitmaskclient.base.models.Provider;
@@ -49,38 +78,10 @@ import se.leap.bitmaskclient.eip.EIP;
import se.leap.bitmaskclient.eip.EipCommand;
import se.leap.bitmaskclient.eip.EipSetupListener;
import se.leap.bitmaskclient.eip.EipSetupObserver;
-import se.leap.bitmaskclient.eip.EipStatus;
import se.leap.bitmaskclient.providersetup.ProviderAPI;
import se.leap.bitmaskclient.providersetup.activities.LoginActivity;
import se.leap.bitmaskclient.providersetup.models.LeapSRPSession;
-import static se.leap.bitmaskclient.R.string.downloading_vpn_certificate_failed;
-import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message;
-import static se.leap.bitmaskclient.base.models.Constants.ASK_TO_CANCEL_VPN;
-import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_CODE;
-import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY;
-import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_LAUNCH_VPN;
-import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_PREPARE_VPN;
-import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START;
-import static se.leap.bitmaskclient.base.models.Constants.EIP_REQUEST;
-import static se.leap.bitmaskclient.base.models.Constants.LOCATION;
-import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
-import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP;
-import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_LOG_IN;
-import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_SWITCH_PROVIDER;
-import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.storeProviderInPreferences;
-import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_INVALID_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_VPN_PREPARE;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORID;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.TOR_EXCEPTION;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.TOR_TIMEOUT;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.USER_MESSAGE;
-
public class MainActivity extends AppCompatActivity implements EipSetupListener, Observer {
@@ -93,6 +94,7 @@ public class MainActivity extends AppCompatActivity implements EipSetupListener,
public final static String ACTION_SHOW_VPN_FRAGMENT = "action_show_vpn_fragment";
public final static String ACTION_SHOW_LOG_FRAGMENT = "action_show_log_fragment";
public final static String ACTION_SHOW_DIALOG_FRAGMENT = "action_show_dialog_fragment";
+ public final static String ACTION_SHOW_MOTD_FRAGMENT = "action_show_motd_fragment";
/**
* Fragment managing the behaviors, interactions and presentation of the navigation drawer.
@@ -161,10 +163,22 @@ public class MainActivity extends AppCompatActivity implements EipSetupListener,
bundle.putParcelable(PROVIDER_KEY, provider);
fragment.setArguments(bundle);
hideActionBarSubTitle();
+ showActionBar();
+ break;
+ case ACTION_SHOW_MOTD_FRAGMENT:
+ fragment = new MotdFragment();
+ Bundle motdBundle = new Bundle();
+ if (intent.hasExtra(EXTRA_MOTD_MSG)) {
+ motdBundle.putString(EXTRA_MOTD_MSG, intent.getStringExtra(EXTRA_MOTD_MSG));
+ }
+ fragment.setArguments(motdBundle);
+ hideActionBarSubTitle();
+ hideActionBar();
break;
case ACTION_SHOW_LOG_FRAGMENT:
fragment = new LogFragment();
setActionBarTitle(R.string.log_fragment_title);
+ showActionBar();
break;
case ACTION_SHOW_DIALOG_FRAGMENT:
if (intent.hasExtra(EIP.ERRORID)) {
@@ -186,6 +200,20 @@ public class MainActivity extends AppCompatActivity implements EipSetupListener,
}
}
+ private void hideActionBar() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.hide();
+ }
+ }
+
+ private void showActionBar() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.show();
+ }
+ }
+
private void hideActionBarSubTitle() {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
index 8d11105f..6e557236 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
@@ -16,6 +16,19 @@
*/
package se.leap.bitmaskclient.base;
+import static se.leap.bitmaskclient.base.MainActivity.ACTION_SHOW_MOTD_FRAGMENT;
+import static se.leap.bitmaskclient.base.MainActivity.ACTION_SHOW_VPN_FRAGMENT;
+import static se.leap.bitmaskclient.base.models.Constants.APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE;
+import static se.leap.bitmaskclient.base.models.Constants.EIP_RESTART_ON_BOOT;
+import static se.leap.bitmaskclient.base.models.Constants.EXTRA_MOTD_MSG;
+import static se.leap.bitmaskclient.base.models.Constants.PREFERENCES_APP_VERSION;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_EIP_DEFINITION;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP;
+import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDefaultBitmask;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.storeProviderInPreferences;
+
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -28,27 +41,22 @@ import androidx.annotation.Nullable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Set;
import de.blinkt.openvpn.core.VpnStatus;
+import motd.IMessage;
+import motd.IMessages;
+import motd.IStringCollection;
+import motd.Motd;
import se.leap.bitmaskclient.BuildConfig;
-import se.leap.bitmaskclient.providersetup.ProviderListActivity;
-import se.leap.bitmaskclient.eip.EipCommand;
import se.leap.bitmaskclient.base.models.FeatureVersionCode;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.base.models.ProviderObservable;
-import se.leap.bitmaskclient.providersetup.activities.CustomProviderSetupActivity;
+import se.leap.bitmaskclient.base.utils.DateHelper;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
-
-import static se.leap.bitmaskclient.base.models.Constants.APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE;
-import static se.leap.bitmaskclient.base.models.Constants.EIP_RESTART_ON_BOOT;
-import static se.leap.bitmaskclient.base.models.Constants.PREFERENCES_APP_VERSION;
-import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_EIP_DEFINITION;
-import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
-import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP;
-import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
-import static se.leap.bitmaskclient.base.MainActivity.ACTION_SHOW_VPN_FRAGMENT;
-import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDefaultBitmask;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.storeProviderInPreferences;
+import se.leap.bitmaskclient.eip.EipCommand;
+import se.leap.bitmaskclient.providersetup.ProviderListActivity;
+import se.leap.bitmaskclient.providersetup.activities.CustomProviderSetupActivity;
/**
* Activity shown at startup. Evaluates if App is started for the first time or has been upgraded
@@ -202,12 +210,13 @@ public class StartActivity extends Activity{
EipCommand.startVPN(this, true);
finish();
} else if (PreferenceHelper.getRestartOnUpdate(this.getApplicationContext())) {
+ // This is relevant for web build flavor apks
PreferenceHelper.restartOnUpdate(this.getApplicationContext(), false);
EipCommand.startVPN(this, false);
- showMainActivity();
+ showNextActivity(provider);
finish();
} else {
- showMainActivity();
+ showNextActivity(provider);
}
} else {
configureLeapProvider();
@@ -234,18 +243,59 @@ public class StartActivity extends Activity{
storeProviderInPreferences(preferences, provider);
ProviderObservable.getInstance().updateProvider(provider);
EipCommand.startVPN(this, false);
- showMainActivity();
+ showNextActivity(provider);
} else if (resultCode == RESULT_CANCELED) {
finish();
}
}
}
- private void showMainActivity() {
+ private void showNextActivity(Provider provider) {
+ if (provider.shouldShowMotdSeen()) {
+ try {
+ IMessages messages = Motd.newMessages(provider.getMotdJsonString());
+
+ IStringCollection stringCollection = Motd.newStringCollection(); //provider.getMotdLastSeenHashCollection();
+ String formattedDate = DateHelper.getFormattedDateWithTimezone(provider.getLastMotdSeen());
+ IMessage message = messages.getFirstMessage(formattedDate, stringCollection);
+ if (message != null) {
+ IMessage imessage = Motd.newMessage(message.toJson());
+ showMotd(provider, imessage);
+ return;
+ }
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Couldn't show Motd. Invalid timestamp.");
+ }
+ }
+ showVPNFragment();
+ }
+
+ private void showMotd(Provider p, IMessage message) {
+ if (message.mType().equals(Motd.MESSAGE_TYPE_ONCE)) {
+ Set<String> lastSeenHashes = p.getMotdLastSeenHashes();
+ String hash = message.hash();
+ lastSeenHashes.add(hash);
+ p.setMotdLastSeenHashes(lastSeenHashes);
+ PreferenceHelper.persistProvider(this, p);
+ ProviderObservable.getInstance().updateProvider(p);
+ }
+ showMotdFragment(message);
+ }
+
+ private void showVPNFragment() {
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.setAction(ACTION_SHOW_VPN_FRAGMENT);
startActivity(intent);
finish();
}
+
+ private void showMotdFragment(IMessage message) {
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.setAction(ACTION_SHOW_MOTD_FRAGMENT);
+ intent.putExtra(EXTRA_MOTD_MSG, message.toJson());
+ startActivity(intent);
+ finish();
+ }
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/MotdFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/MotdFragment.java
new file mode 100644
index 00000000..16834ab5
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/MotdFragment.java
@@ -0,0 +1,103 @@
+package se.leap.bitmaskclient.base.fragments;
+
+import static se.leap.bitmaskclient.base.MainActivity.ACTION_SHOW_VPN_FRAGMENT;
+import static se.leap.bitmaskclient.base.models.Constants.EXTRA_MOTD_MSG;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatImageButton;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.fragment.app.Fragment;
+
+import java.util.Locale;
+
+import de.blinkt.openvpn.core.VpnStatus;
+import motd.IMessage;
+import motd.Motd;
+import se.leap.bitmaskclient.base.MainActivity;
+import se.leap.bitmaskclient.databinding.FMotdBinding;
+
+
+public class MotdFragment extends Fragment {
+
+ private static final String TAG = MotdFragment.class.getSimpleName();
+ private IMessage message;
+ FMotdBinding binding;
+ AppCompatTextView messageView;
+ AppCompatImageButton nextButton;
+
+ public MotdFragment() {
+ // Required empty public constructor
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getArguments() != null) {
+ String messageString = getArguments().getString(EXTRA_MOTD_MSG);
+ if (messageString != null) {
+ Log.d(TAG, "MotdFragment received: " + messageString);
+ message = Motd.newMessage(messageString);
+ }
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ binding = FMotdBinding.inflate(getLayoutInflater());
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ messageView = binding.motdContent;
+ nextButton = binding.nextBtn;
+ String currentLang = Locale.getDefault().getLanguage();
+ String text = message.getLocalizedText(currentLang);
+ if (TextUtils.isEmpty(text)) {
+ text = message.getLocalizedText("en");
+ }
+
+ if (TextUtils.isEmpty(text)) {
+ String error = "Message of the day cannot be shown. Unsupported app language and unknown default langauge.";
+ Log.e(TAG, error);
+ VpnStatus.logError(error);
+ showVpnFragment(view.getContext());
+ return;
+ }
+
+ Log.d(TAG, "set motd text: " + text);
+ messageView.setText(Html.fromHtml(text));
+ messageView.setMovementMethod(LinkMovementMethod.getInstance());
+ nextButton.setOnClickListener(v -> {
+ showVpnFragment(v.getContext());
+ });
+
+ }
+
+ private void showVpnFragment(Context context) {
+ try {
+ Intent intent = new Intent(context, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.setAction(ACTION_SHOW_VPN_FRAGMENT);
+ context.startActivity(intent);
+ } catch (NullPointerException npe) {
+ npe.printStackTrace();
+ }
+
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java
index 6829d9f1..2c0fdd69 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java
@@ -7,7 +7,6 @@ import android.app.Dialog;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
-import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -15,9 +14,6 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDialogFragment;
import androidx.appcompat.widget.AppCompatButton;
import androidx.appcompat.widget.AppCompatEditText;
-import androidx.appcompat.widget.AppCompatSpinner;
-
-import java.util.ArrayList;
import se.leap.bitmaskclient.base.utils.ConfigHelper.ObfsVpnHelper;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
index fd9f2a9b..ee5bd2a7 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
@@ -53,7 +53,7 @@ public interface Constants {
String OBFUSCATION_PINNING_LOCATION = "obfuscation_pinning_location";
- //////////////////////////////////////////////
+ //////////////////////////////////////////////
// REQUEST CODE CONSTANTS
/////////////////////////////////////////////
@@ -77,6 +77,7 @@ public interface Constants {
String ASK_TO_CANCEL_VPN = "ask_to_cancel_vpn";
+ String EXTRA_MOTD_MSG = "extra_motd_message";
//////////////////////////////////////////////
@@ -114,6 +115,10 @@ public interface Constants {
String PROVIDER_EIP_DEFINITION = "Constants.EIP_DEFINITION";
String PROVIDER_PROFILE_UUID = "Constants.PROVIDER_PROFILE_UUID";
String PROVIDER_PROFILE = "Constants.PROVIDER_PROFILE";
+ String PROVIDER_MOTD = "Constants.PROVIDER_MOTD";
+ String PROVIDER_MOTD_HASHES = "Constants.PROVIDER_MOTD_HASHES";
+ String PROVIDER_MOTD_LAST_SEEN = "Constants.PROVIDER_MOTD_LAST_SEEN";
+ String PROVIDER_MOTD_LAST_UPDATED = "Constants.PROVIDER_MOTD_LAST_UPDATED";
////////////////////////////////////////////////
// PRESHIPPED PROVIDER CONFIG
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
index 13463167..53f864cf 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
@@ -16,9 +16,23 @@
*/
package se.leap.bitmaskclient.base.models;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_KCP;
+import static se.leap.bitmaskclient.base.models.Constants.CAPABILITIES;
+import static se.leap.bitmaskclient.base.models.Constants.GATEWAYS;
+import static se.leap.bitmaskclient.base.models.Constants.LOCATIONS;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOWED_REGISTERED;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOW_ANONYMOUS;
+import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT;
+import static se.leap.bitmaskclient.base.models.Constants.TYPE;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.ObfsVpnHelper.useObfsVpn;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
+
import android.os.Parcel;
import android.os.Parcelable;
+import androidx.annotation.NonNull;
+
import com.google.gson.Gson;
import org.json.JSONArray;
@@ -27,22 +41,14 @@ import org.json.JSONObject;
import java.net.MalformedURLException;
import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashSet;
import java.util.Locale;
+import java.util.Set;
-import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
-import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_KCP;
-import static se.leap.bitmaskclient.base.models.Constants.CAPABILITIES;
-import static se.leap.bitmaskclient.base.models.Constants.GATEWAYS;
-import static se.leap.bitmaskclient.base.models.Constants.LOCATIONS;
-import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOWED_REGISTERED;
-import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOW_ANONYMOUS;
-import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT;
-import static se.leap.bitmaskclient.base.models.Constants.TYPE;
-import static se.leap.bitmaskclient.base.utils.ConfigHelper.ObfsVpnHelper.useObfsVpn;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
-
-import de.blinkt.openvpn.core.connection.Connection;
import de.blinkt.openvpn.core.connection.Connection.TransportType;
+import motd.IStringCollection;
+import motd.Motd;
/**
* @author Sean Leonard <meanderingcode@aetherislands.net>
@@ -50,14 +56,17 @@ import de.blinkt.openvpn.core.connection.Connection.TransportType;
*/
public final class Provider implements Parcelable {
- private static long EIP_SERVICE_TIMEOUT = 1000 * 60 * 60 * 24 * 3;
- private static long GEOIP_SERVICE_TIMEOUT = 1000 * 60 * 60;
+ private static final long EIP_SERVICE_TIMEOUT = 1000 * 60 * 60 * 24 * 3;
+ private static final long GEOIP_SERVICE_TIMEOUT = 1000 * 60 * 60;
+ private static final long MOTD_TIMEOUT = 1000 * 60 * 60 * 24;
private JSONObject definition = new JSONObject(); // Represents our Provider's provider.json
private JSONObject eipServiceJson = new JSONObject();
private JSONObject geoIpJson = new JSONObject();
+ private JSONObject motdJson = new JSONObject();
private DefaultedURL mainUrl = new DefaultedURL();
private DefaultedURL apiUrl = new DefaultedURL();
private DefaultedURL geoipUrl = new DefaultedURL();
+ private DefaultedURL motdUrl = new DefaultedURL();
private String domain = "";
private String providerIp = ""; // ip of the provider main url
private String providerApiIp = ""; // ip of the provider api url
@@ -69,6 +78,9 @@ public final class Provider implements Parcelable {
private String vpnCertificate = "";
private long lastEipServiceUpdate = 0L;
private long lastGeoIpUpdate = 0L;
+ private long lastMotdUpdate = 0L;
+ private long lastMotdSeen = 0L;
+ private Set<String> lastMotdSeenHashes = new HashSet<>();
private boolean shouldUpdateVpnCertificate;
private boolean allowAnonymous;
@@ -90,7 +102,8 @@ public final class Provider implements Parcelable {
MAIN_URL = "main_url",
PROVIDER_IP = "provider_ip",
PROVIDER_API_IP = "provider_api_ip",
- GEOIP_URL = "geoip_url";
+ GEOIP_URL = "geoip_url",
+ MOTD_URL = "motd_url";
private static final String API_TERM_NAME = "name";
@@ -110,10 +123,10 @@ public final class Provider implements Parcelable {
}
public Provider(String mainUrl, String providerIp, String providerApiIp) {
- this(mainUrl, null, providerIp, providerApiIp);
+ this(mainUrl, null, null, providerIp, providerApiIp);
}
- public Provider(String mainUrl, String geoipUrl, String providerIp, String providerApiIp) {
+ public Provider(String mainUrl, String geoipUrl, String motdUrl, String providerIp, String providerApiIp) {
try {
this.mainUrl.setUrl(new URL(mainUrl));
if (providerIp != null) {
@@ -127,11 +140,11 @@ public final class Provider implements Parcelable {
return;
}
setGeoipUrl(geoipUrl);
+ setMotdUrl(motdUrl);
}
-
- public Provider(String mainUrl, String geoipUrl, String providerIp, String providerApiIp, String caCert, String definition) {
- this(mainUrl, geoipUrl, providerIp, providerApiIp);
+ public Provider(String mainUrl, String geoipUrl, String motdUrl, String providerIp, String providerApiIp, String caCert, String definition) {
+ this(mainUrl, geoipUrl, motdUrl, providerIp, providerApiIp);
if (caCert != null) {
this.caCert = caCert;
}
@@ -281,6 +294,18 @@ public final class Provider implements Parcelable {
}
}
+ public DefaultedURL getMotdUrl() {
+ return this.motdUrl;
+ }
+
+ public void setMotdUrl(String url) {
+ try {
+ this.motdUrl.setUrl(new URL(url));
+ } catch (MalformedURLException e) {
+ this.motdUrl = new DefaultedURL();
+ }
+ }
+
public String getApiUrlWithVersion() {
return getApiUrlString() + "/" + getApiVersion();
}
@@ -375,14 +400,19 @@ public final class Provider implements Parcelable {
parcel.writeString(getProviderIp());
parcel.writeString(getProviderApiIp());
parcel.writeString(getGeoipUrl().toString());
+ parcel.writeString(getMotdUrl().toString());
parcel.writeString(getDefinitionString());
parcel.writeString(getCaCert());
parcel.writeString(getEipServiceJsonString());
parcel.writeString(getGeoIpJsonString());
+ parcel.writeString(getMotdJsonString());
parcel.writeString(getPrivateKey());
parcel.writeString(getVpnCertificate());
parcel.writeLong(lastEipServiceUpdate);
parcel.writeLong(lastGeoIpUpdate);
+ parcel.writeLong(lastMotdUpdate);
+ parcel.writeLong(lastMotdSeen);
+ parcel.writeStringList(new ArrayList<>(lastMotdSeenHashes));
parcel.writeInt(shouldUpdateVpnCertificate ? 0 : 1);
}
@@ -406,6 +436,10 @@ public final class Provider implements Parcelable {
}
tmpString = in.readString();
if (!tmpString.isEmpty()) {
+ motdUrl.setUrl(new URL(tmpString));
+ }
+ tmpString = in.readString();
+ if (!tmpString.isEmpty()) {
definition = new JSONObject((tmpString));
parseDefinition(definition);
}
@@ -423,6 +457,10 @@ public final class Provider implements Parcelable {
}
tmpString = in.readString();
if (!tmpString.isEmpty()) {
+ this.setMotdJson(new JSONObject(tmpString));
+ }
+ tmpString = in.readString();
+ if (!tmpString.isEmpty()) {
this.setPrivateKey(tmpString);
}
tmpString = in.readString();
@@ -431,6 +469,11 @@ public final class Provider implements Parcelable {
}
this.lastEipServiceUpdate = in.readLong();
this.lastGeoIpUpdate = in.readLong();
+ this.lastMotdUpdate = in.readLong();
+ this.lastMotdSeen = in.readLong();
+ ArrayList<String> lastMotdSeenHashes = new ArrayList<>();
+ in.readStringList(lastMotdSeenHashes);
+ this.lastMotdSeenHashes = new HashSet<>(lastMotdSeenHashes);
this.shouldUpdateVpnCertificate = in.readInt() == 0;
} catch (MalformedURLException | JSONException e) {
e.printStackTrace();
@@ -447,10 +490,12 @@ public final class Provider implements Parcelable {
definition.toString().equals(p.getDefinition().toString()) &&
eipServiceJson.toString().equals(p.getEipServiceJsonString()) &&
geoIpJson.toString().equals(p.getGeoIpJsonString()) &&
+ motdJson.toString().equals(p.getMotdJsonString()) &&
providerIp.equals(p.getProviderIp()) &&
providerApiIp.equals(p.getProviderApiIp()) &&
apiUrl.equals(p.getApiUrl()) &&
geoipUrl.equals(p.getGeoipUrl()) &&
+ motdUrl.equals(p.getMotdUrl()) &&
certificatePin.equals(p.getCertificatePin()) &&
certificatePinEncoding.equals(p.getCertificatePinEncoding()) &&
caCert.equals(p.getCaCert()) &&
@@ -527,6 +572,70 @@ public final class Provider implements Parcelable {
return shouldUpdateVpnCertificate;
}
+ public void setLastMotdSeen(long timestamp) {
+ lastMotdSeen = timestamp;
+ }
+
+ public long getLastMotdSeen() {
+ return lastMotdSeen;
+ }
+
+ /**
+ * shouldShowMotdSeen
+ * @return true if last message of the day was shown more than 24h ago
+ */
+ public boolean shouldShowMotdSeen() {
+ return !motdUrl.isDefault() && System.currentTimeMillis() - lastMotdSeen >= MOTD_TIMEOUT;
+ }
+
+ /**
+ * setLastSeenHashes
+ * @param hashes hashes of messages of type 'once' that have already been seen
+ */
+ public void setMotdLastSeenHashes(Set<String> hashes) {
+ lastMotdSeenHashes = hashes;
+ }
+
+ public Set<String> getMotdLastSeenHashes() {
+ return lastMotdSeenHashes;
+ }
+
+ /**
+ * getLastSeenHashCollection
+ * @return go ffi compatible IStringCollection interface of message hashes of type 'once'
+ */
+ public IStringCollection getMotdLastSeenHashCollection() {
+ IStringCollection stringCollection = Motd.newStringCollection();
+ for (String hash : lastMotdSeenHashes) {
+ stringCollection.add(hash);
+ }
+ return stringCollection;
+ }
+
+ public void setLastMotdUpdate(long timestamp) {
+ lastMotdUpdate = timestamp;
+ }
+
+ public long getLastMotdUpdate() {
+ return lastMotdUpdate;
+ }
+
+ public boolean shouldUpdateMotdJson() {
+ return !motdUrl.isDefault() && System.currentTimeMillis() - lastMotdUpdate >= MOTD_TIMEOUT;
+ }
+
+ public void setMotdJson(@NonNull JSONObject motdJson) {
+ this.motdJson = motdJson;
+ }
+
+ public JSONObject getMotdJson() {
+ return motdJson;
+ }
+
+ public String getMotdJsonString() {
+ return motdJson.toString();
+ }
+
public void setLastGeoIpUpdate(long timestamp) {
lastGeoIpUpdate = timestamp;
}
@@ -621,6 +730,7 @@ public final class Provider implements Parcelable {
definition = new JSONObject();
eipServiceJson = new JSONObject();
geoIpJson = new JSONObject();
+ motdJson = new JSONObject();
apiUrl = new DefaultedURL();
certificatePin = "";
certificatePinEncoding = "";
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java
index 0476bf12..2e9c2b24 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java
@@ -12,6 +12,7 @@ import java.util.Locale;
*/
public class DateHelper {
private static final String DATE_PATTERN = "dd/MM/yyyy";
+ private static final String DATE_PATTERN_RFC822_NUMERIC_ZONE = "dd MMM yy hh:mm Z"; // RFC822 with numeric zone
private static final int ONE_DAY = 86400000; //1000*60*60*24
public static long getDateDiffToCurrentDateInDays(String startDate) throws ParseException {
@@ -26,4 +27,14 @@ public class DateHelper {
Date lastDate = new Date();
return sdf.format(lastDate);
}
+
+ /**
+ * Return date in a specific format bitmaskcore can handle.
+ * @param milliSeconds Date in milliseconds
+ * @return String representing date in specified format
+ */
+ public static String getFormattedDateWithTimezone(long milliSeconds) throws IllegalArgumentException {
+ SimpleDateFormat sdf = new SimpleDateFormat(DATE_PATTERN_RFC822_NUMERIC_ZONE, Locale.US);
+ return sdf.format(milliSeconds);
+ }
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
index 4abeed1a..76e54794 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
@@ -20,6 +20,10 @@ import static se.leap.bitmaskclient.base.models.Constants.PREFERRED_CITY;
import static se.leap.bitmaskclient.base.models.Constants.PREFER_UDP;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_CONFIGURED;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_EIP_DEFINITION;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_HASHES;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_LAST_SEEN;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_LAST_UPDATED;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PRIVATE_KEY;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.base.models.Constants.RESTART_ON_UPDATE;
@@ -62,11 +66,16 @@ public class PreferenceHelper {
provider.setProviderIp(preferences.getString(Provider.PROVIDER_IP, ""));
provider.setProviderApiIp(preferences.getString(Provider.PROVIDER_API_IP, ""));
provider.setGeoipUrl(preferences.getString(Provider.GEOIP_URL, ""));
+ provider.setMotdUrl(preferences.getString(Provider.MOTD_URL, ""));
provider.define(new JSONObject(preferences.getString(Provider.KEY, "")));
provider.setCaCert(preferences.getString(Provider.CA_CERT, ""));
provider.setVpnCertificate(preferences.getString(PROVIDER_VPN_CERTIFICATE, ""));
provider.setPrivateKey(preferences.getString(PROVIDER_PRIVATE_KEY, ""));
provider.setEipServiceJson(new JSONObject(preferences.getString(PROVIDER_EIP_DEFINITION, "")));
+ provider.setMotdJson(new JSONObject(preferences.getString(PROVIDER_MOTD, "")));
+ provider.setLastMotdSeen(preferences.getLong(PROVIDER_MOTD_LAST_SEEN, 0L));
+ provider.setLastMotdUpdate(preferences.getLong(PROVIDER_MOTD_LAST_UPDATED, 0L));
+ provider.setMotdLastSeenHashes(preferences.getStringSet(PROVIDER_MOTD_HASHES, new HashSet<>()));
} catch (MalformedURLException | JSONException e) {
e.printStackTrace();
}
@@ -78,12 +87,31 @@ public class PreferenceHelper {
return preferences.getString(toFetch + "." + providerDomain, "");
}
+ public static long getLongFromPersistedProvider(String toFetch, String providerDomain, SharedPreferences preferences) {
+ return preferences.getLong(toFetch + "." + providerDomain, 0L);
+ }
+
+ public static Set<String> getStringSetFromPersistedProvider(String toFetch, String providerDomain, SharedPreferences preferences) {
+ return preferences.getStringSet(toFetch + "." + providerDomain, new HashSet<>());
+ }
+
+ public static void persistProvider(Context context, Provider provider) {
+ SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ storeProviderInPreferences(preferences, provider, true);
+ }
+
+ public static void storeProviderInPreferences(SharedPreferences preferences, Provider provider) {
+ storeProviderInPreferences(preferences, provider, false);
+ }
+
// TODO: replace commit with apply after refactoring EIP
//FIXME: don't save private keys in shared preferences! use the keystore
- public static void storeProviderInPreferences(SharedPreferences preferences, Provider provider) {
- preferences.edit().putBoolean(PROVIDER_CONFIGURED, true).
+ public static void storeProviderInPreferences(SharedPreferences preferences, Provider provider, boolean apply) {
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putBoolean(PROVIDER_CONFIGURED, true).
putString(Provider.PROVIDER_IP, provider.getProviderIp()).
putString(Provider.GEOIP_URL, provider.getGeoipUrl().toString()).
+ putString(Provider.MOTD_URL, provider.getMotdUrl().toString()).
putString(Provider.PROVIDER_API_IP, provider.getProviderApiIp()).
putString(Provider.MAIN_URL, provider.getMainUrlString()).
putString(Provider.KEY, provider.getDefinitionString()).
@@ -91,7 +119,15 @@ public class PreferenceHelper {
putString(PROVIDER_EIP_DEFINITION, provider.getEipServiceJsonString()).
putString(PROVIDER_PRIVATE_KEY, provider.getPrivateKey()).
putString(PROVIDER_VPN_CERTIFICATE, provider.getVpnCertificate()).
- commit();
+ putString(PROVIDER_MOTD, provider.getMotdJsonString()).
+ putStringSet(PROVIDER_MOTD_HASHES, provider.getMotdLastSeenHashes()).
+ putLong(PROVIDER_MOTD_LAST_SEEN, provider.getLastMotdSeen()).
+ putLong(PROVIDER_MOTD_LAST_UPDATED, provider.getLastMotdUpdate());
+ if (apply) {
+ editor.apply();
+ } else {
+ editor.commit();
+ }
String providerDomain = provider.getDomain();
preferences.edit().putBoolean(PROVIDER_CONFIGURED, true).
@@ -99,9 +135,14 @@ public class PreferenceHelper {
putString(Provider.PROVIDER_API_IP + "." + providerDomain, provider.getProviderApiIp()).
putString(Provider.MAIN_URL + "." + providerDomain, provider.getMainUrlString()).
putString(Provider.GEOIP_URL + "." + providerDomain, provider.getGeoipUrl().toString()).
+ putString(Provider.MOTD_URL + "." + providerDomain, provider.getMotdUrl().toString()).
putString(Provider.KEY + "." + providerDomain, provider.getDefinitionString()).
putString(Provider.CA_CERT + "." + providerDomain, provider.getCaCert()).
putString(PROVIDER_EIP_DEFINITION + "." + providerDomain, provider.getEipServiceJsonString()).
+ putString(PROVIDER_MOTD + "." + providerDomain, provider.getMotdJsonString()).
+ putStringSet(PROVIDER_MOTD_HASHES + "." + providerDomain, provider.getMotdLastSeenHashes()).
+ putLong(PROVIDER_MOTD_LAST_SEEN + "." + providerDomain, provider.getLastMotdSeen()).
+ putLong(PROVIDER_MOTD_LAST_UPDATED + "." + providerDomain, provider.getLastMotdUpdate()).
apply();
}
@@ -132,9 +173,14 @@ public class PreferenceHelper {
remove(Provider.PROVIDER_API_IP + "." + providerDomain).
remove(Provider.MAIN_URL + "." + providerDomain).
remove(Provider.GEOIP_URL + "." + providerDomain).
+ remove(Provider.MOTD_URL + "." + providerDomain).
remove(PROVIDER_EIP_DEFINITION + "." + providerDomain).
remove(PROVIDER_PRIVATE_KEY + "." + providerDomain).
remove(PROVIDER_VPN_CERTIFICATE + "." + providerDomain).
+ remove(PROVIDER_MOTD + "." + providerDomain).
+ remove(PROVIDER_MOTD_HASHES + "." + providerDomain).
+ remove(PROVIDER_MOTD_LAST_SEEN + "." + providerDomain).
+ remove(PROVIDER_MOTD_LAST_UPDATED + "." + providerDomain).
apply();
}
@@ -146,9 +192,14 @@ public class PreferenceHelper {
remove(Provider.PROVIDER_API_IP).
remove(Provider.MAIN_URL).
remove(Provider.GEOIP_URL).
+ remove(Provider.MOTD_URL).
remove(PROVIDER_EIP_DEFINITION).
remove(PROVIDER_PRIVATE_KEY).
remove(PROVIDER_VPN_CERTIFICATE).
+ remove(PROVIDER_MOTD).
+ remove(PROVIDER_MOTD_HASHES).
+ remove(PROVIDER_MOTD_LAST_SEEN).
+ remove(PROVIDER_MOTD_LAST_UPDATED).
apply();
}
@@ -360,10 +411,7 @@ public class PreferenceHelper {
}
public static void setExcludedApps(Context context, Set<String> apps) {
- SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
- SharedPreferences.Editor prefsedit = prefs.edit();
- prefsedit.putStringSet(EXCLUDED_APPS, apps);
- prefsedit.apply();
+ putStringSet(context, EXCLUDED_APPS, apps);
}
public static Set<String> getExcludedApps(Context context) {
@@ -415,6 +463,14 @@ public class PreferenceHelper {
preferences.edit().putString(key, value).apply();
}
+ public static void putStringSet(Context context, String key, Set<String> value) {
+ if (context == null) {
+ return;
+ }
+ SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ preferences.edit().putStringSet(key, value).apply();
+ }
+
public static boolean getBoolean(Context context, String key, Boolean defValue) {
if (context == null) {
return false;
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java b/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java
index 05991390..85cdb06c 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java
@@ -40,6 +40,7 @@ import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOAD
import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_GEOIP_JSON;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.DELAY;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_MOTD;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_GEOIP_JSON;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
@@ -385,6 +386,12 @@ public class EipSetupObserver extends BroadcastReceiver implements VpnStatus.Sta
parameters.putLong(DELAY, 500);
ProviderAPICommand.execute(appContext, QUIETLY_UPDATE_VPN_CERTIFICATE, parameters, provider);
}
+
+ if (provider.shouldUpdateMotdJson()) {
+ Bundle parameters = new Bundle();
+ parameters.putLong(DELAY, 500);
+ ProviderAPICommand.execute(appContext, DOWNLOAD_MOTD, parameters, provider);
+ }
finishGatewaySetup(false);
} else if ("TCP_CONNECT".equals(state)) {
changingGateway.set(false);
diff --git a/app/src/main/java/se/leap/bitmaskclient/motd/MotdClient.java b/app/src/main/java/se/leap/bitmaskclient/motd/MotdClient.java
new file mode 100644
index 00000000..d80a8992
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/motd/MotdClient.java
@@ -0,0 +1,47 @@
+package se.leap.bitmaskclient.motd;
+
+import androidx.annotation.WorkerThread;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import de.blinkt.openvpn.core.VpnStatus;
+import motd.IMessages;
+import motd.IMotd;
+import motd.Motd;
+import se.leap.bitmaskclient.base.models.Provider;
+
+public class MotdClient {
+ IMotd motd;
+
+ public MotdClient(Provider provider) {
+ motd = Motd.newMotd(provider.getMotdUrl().toString(), provider.getName(), "android");
+ }
+
+ @WorkerThread
+ public IMessages fetch() {
+ if (!VpnStatus.isVPNActive()) {
+ VpnStatus.logError("Tried to fetch Message of the Day while VPN was off.");
+ return null;
+ }
+
+ return Motd.newMessages(motd.fetchLatestAsJson());
+ }
+
+ @WorkerThread
+ public JSONObject fetchJson() {
+ if (!VpnStatus.isVPNActive()) {
+ VpnStatus.logError("Tried to fetch Message of the Day while VPN was off.");
+ return null;
+ }
+
+ try {
+ return new JSONObject(motd.fetchLatestAsJson());
+ } catch (NullPointerException | JSONException e) {
+ e.printStackTrace();
+ return null;
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java
index e45add50..86ce577b 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java
@@ -60,6 +60,7 @@ public class ProviderAPI extends JobIntentService implements ProviderApiManagerB
SET_UP_PROVIDER = "setUpProvider",
UPDATE_PROVIDER_DETAILS = "updateProviderDetails",
DOWNLOAD_GEOIP_JSON = "downloadGeoIpJson",
+ DOWNLOAD_MOTD = "downloadMotd",
SIGN_UP = "srpRegister",
LOG_IN = "srpAuth",
LOG_OUT = "logOut",
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java
index 7b6a3ad6..14308875 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java
@@ -35,10 +35,15 @@ import static se.leap.bitmaskclient.base.models.Constants.CREDENTIALS_PASSWORD;
import static se.leap.bitmaskclient.base.models.Constants.CREDENTIALS_USERNAME;
import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_HASHES;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_LAST_SEEN;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_LAST_UPDATED;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PRIVATE_KEY;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.base.models.Provider.CA_CERT;
import static se.leap.bitmaskclient.base.models.Provider.GEOIP_URL;
+import static se.leap.bitmaskclient.base.models.Provider.MOTD_URL;
import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_API_IP;
import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_IP;
import static se.leap.bitmaskclient.base.utils.ConfigHelper.getDomainFromMainURL;
@@ -47,6 +52,8 @@ import static se.leap.bitmaskclient.base.utils.ConfigHelper.getProviderFormatted
import static se.leap.bitmaskclient.base.utils.ConfigHelper.parseRsaKeyFromString;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.deleteProviderDetailsFromPreferences;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getFromPersistedProvider;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getLongFromPersistedProvider;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getStringSetFromPersistedProvider;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.BACKEND_ERROR_KEY;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.BACKEND_ERROR_MESSAGE;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE;
@@ -55,6 +62,7 @@ import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOAD
import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.DELAY;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_GEOIP_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_MOTD;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_SERVICE_JSON;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORID;
@@ -124,6 +132,7 @@ import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
+import java.util.Set;
import java.util.concurrent.TimeoutException;
import javax.net.ssl.SSLHandshakeException;
@@ -138,6 +147,7 @@ import se.leap.bitmaskclient.base.models.ProviderObservable;
import se.leap.bitmaskclient.base.utils.ConfigHelper;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
import se.leap.bitmaskclient.eip.EipStatus;
+import se.leap.bitmaskclient.motd.MotdClient;
import se.leap.bitmaskclient.providersetup.connectivity.OkHttpClientGenerator;
import se.leap.bitmaskclient.providersetup.models.LeapSRPSession;
import se.leap.bitmaskclient.providersetup.models.SrpCredentials;
@@ -299,6 +309,17 @@ public abstract class ProviderApiManagerBase {
}
ProviderObservable.getInstance().setProviderForDns(null);
break;
+ case DOWNLOAD_MOTD:
+ MotdClient client = new MotdClient(provider);
+ JSONObject motd = client.fetchJson();
+ if (motd != null) {
+ provider.setMotdJson(motd);
+ provider.setLastMotdUpdate(System.currentTimeMillis());
+ }
+ PreferenceHelper.storeProviderInPreferences(preferences, provider);
+ ProviderObservable.getInstance().updateProvider(provider);
+ break;
+
case UPDATE_INVALID_VPN_CERTIFICATE:
ProviderObservable.getInstance().setProviderForDns(provider);
result = updateVpnCertificate(provider);
@@ -914,7 +935,11 @@ public abstract class ProviderApiManagerBase {
provider.setVpnCertificate(getPersistedVPNCertificate(providerDomain));
provider.setProviderApiIp(getPersistedProviderApiIp(providerDomain));
provider.setProviderIp(getPersistedProviderIp(providerDomain));
- provider.setGeoipUrl(getPersistedGeoIp(providerDomain));
+ provider.setGeoipUrl(getPersistedGeoIp(providerDomain)); // TODO: do we really need to persist the Geoip URL??
+ provider.setLastMotdSeen(getPersistedMotdLastSeen(providerDomain));
+ provider.setMotdLastSeenHashes(getPersistedMotdHashes(providerDomain));
+ provider.setLastMotdUpdate(getPersistedMotdLastUpdate(providerDomain));
+ provider.setMotdJson(getPersistedMotd(providerDomain));
}
}
@@ -1045,6 +1070,27 @@ public abstract class ProviderApiManagerBase {
return getFromPersistedProvider(GEOIP_URL, providerDomain, preferences);
}
+ protected JSONObject getPersistedMotd(String providerDomain) {
+ try {
+ return new JSONObject(getFromPersistedProvider(PROVIDER_MOTD, providerDomain, preferences));
+ } catch (JSONException e) {
+ return new JSONObject();
+ }
+ }
+
+ protected long getPersistedMotdLastSeen(String providerDomain) {
+ return getLongFromPersistedProvider(PROVIDER_MOTD_LAST_SEEN, providerDomain, preferences);
+ }
+
+ protected long getPersistedMotdLastUpdate(String providerDomain) {
+ return getLongFromPersistedProvider(PROVIDER_MOTD_LAST_UPDATED, providerDomain, preferences);
+ }
+
+ protected Set<String> getPersistedMotdHashes(String providerDomain) {
+ return getStringSetFromPersistedProvider(PROVIDER_MOTD_HASHES, providerDomain, preferences);
+ }
+
+
protected boolean hasUpdatedProviderDetails(String domain) {
return preferences.contains(Provider.KEY + "." + domain) && preferences.contains(CA_CERT + "." + domain);
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
index 88413087..1ae2a033 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
@@ -24,6 +24,7 @@ import static se.leap.bitmaskclient.base.models.Constants.EXT_PEM;
import static se.leap.bitmaskclient.base.models.Constants.URLS;
import static se.leap.bitmaskclient.base.models.Provider.GEOIP_URL;
import static se.leap.bitmaskclient.base.models.Provider.MAIN_URL;
+import static se.leap.bitmaskclient.base.models.Provider.MOTD_URL;
import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_API_IP;
import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_IP;
import static se.leap.bitmaskclient.base.utils.FileHelper.createFile;
@@ -91,6 +92,7 @@ public class ProviderManager implements AdapteeCollection<Provider> {
String certificate = null;
String providerDefinition = null;
String geoipUrl = null;
+ String motdUrl = null;
try {
String provider = file.substring(0, file.length() - ".url".length());
InputStream providerFile = assetsManager.open(directory + "/" + file);
@@ -98,12 +100,13 @@ public class ProviderManager implements AdapteeCollection<Provider> {
providerIp = extractKeyFromInputStream(providerFile, PROVIDER_IP);
providerApiIp = extractKeyFromInputStream(providerFile, PROVIDER_API_IP);
geoipUrl = extractKeyFromInputStream(providerFile, GEOIP_URL);
+ motdUrl = extractKeyFromInputStream(providerFile, MOTD_URL);
certificate = loadInputStreamAsString(assetsManager.open(provider + EXT_PEM));
providerDefinition = loadInputStreamAsString(assetsManager.open(provider + EXT_JSON));
} catch (IOException e) {
e.printStackTrace();
}
- providers.add(new Provider(mainUrl, geoipUrl, providerIp, providerApiIp, certificate, providerDefinition));
+ providers.add(new Provider(mainUrl, geoipUrl, motdUrl, providerIp, providerApiIp, certificate, providerDefinition));
}
return providers;
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CustomProviderSetupActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CustomProviderSetupActivity.java
index 0fff1ee2..9cd46049 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CustomProviderSetupActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CustomProviderSetupActivity.java
@@ -36,6 +36,7 @@ import se.leap.bitmaskclient.providersetup.ProviderAPICommand;
import static se.leap.bitmaskclient.BuildConfig.customProviderApiIp;
import static se.leap.bitmaskclient.BuildConfig.customProviderIp;
+import static se.leap.bitmaskclient.BuildConfig.customProviderMotdUrl;
import static se.leap.bitmaskclient.BuildConfig.customProviderUrl;
import static se.leap.bitmaskclient.BuildConfig.geoipUrl;
import static se.leap.bitmaskclient.base.models.Constants.EXT_JSON;
@@ -72,7 +73,7 @@ public class CustomProviderSetupActivity extends ProviderSetupBaseActivity {
private void setDefaultProvider() {
try {
AssetManager assetsManager = getAssets();
- Provider customProvider = new Provider(customProviderUrl, geoipUrl, customProviderIp, customProviderApiIp);
+ Provider customProvider = new Provider(customProviderUrl, geoipUrl, customProviderMotdUrl, customProviderIp, customProviderApiIp);
String domain = ConfigHelper.getDomainFromMainURL(customProviderUrl);
String certificate = loadInputStreamAsString(assetsManager.open(domain + EXT_PEM));
String providerDefinition = loadInputStreamAsString(assetsManager.open(domain + EXT_JSON));
@@ -81,7 +82,7 @@ public class CustomProviderSetupActivity extends ProviderSetupBaseActivity {
setProvider(customProvider);
} catch (IOException | JSONException e) {
e.printStackTrace();
- setProvider(new Provider(customProviderUrl, geoipUrl, customProviderIp, customProviderApiIp));
+ setProvider(new Provider(customProviderUrl, geoipUrl, customProviderMotdUrl, customProviderIp, customProviderApiIp));
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java
index 002335db..90ebfb4d 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java
@@ -95,6 +95,7 @@ public abstract class ProviderListBaseActivity extends ProviderSetupBaseActivity
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CONFIGURE_LEAP) {
if (resultCode == RESULT_OK) {
setResult(resultCode, data);
diff --git a/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java b/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java
index 59c1290a..5f7fa74a 100644
--- a/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java
+++ b/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java
@@ -19,7 +19,6 @@ import android.content.Context;
import android.os.FileObserver;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.torproject.jni.ClientTransportPluginInterface;
diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_right.png b/app/src/main/res/drawable-hdpi/ic_arrow_right.png
new file mode 100644
index 00000000..2d081384
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_arrow_right.png
Binary files differ
diff --git a/app/src/main/res/drawable-ldpi/ic_arrow_right.png b/app/src/main/res/drawable-ldpi/ic_arrow_right.png
new file mode 100644
index 00000000..7fc572af
--- /dev/null
+++ b/app/src/main/res/drawable-ldpi/ic_arrow_right.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_right.png b/app/src/main/res/drawable-mdpi/ic_arrow_right.png
new file mode 100644
index 00000000..60702e35
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_arrow_right.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_right.png b/app/src/main/res/drawable-xhdpi/ic_arrow_right.png
new file mode 100644
index 00000000..f1f4f691
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_arrow_right.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_right.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_right.png
new file mode 100644
index 00000000..e0031ca8
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_arrow_right.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_arrow_right.png b/app/src/main/res/drawable-xxxhdpi/ic_arrow_right.png
new file mode 100644
index 00000000..1b4f4239
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/ic_arrow_right.png
Binary files differ
diff --git a/app/src/main/res/drawable/background_motd.xml b/app/src/main/res/drawable/background_motd.xml
new file mode 100644
index 00000000..519e0233
--- /dev/null
+++ b/app/src/main/res/drawable/background_motd.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle"
+ >
+ <gradient android:startColor="@color/black_high_transparent"
+ android:endColor="@color/white_transparent"
+ android:angle="90"
+ />
+</shape> \ No newline at end of file
diff --git a/app/src/main/res/drawable/motd_img.xml b/app/src/main/res/drawable/motd_img.xml
new file mode 100644
index 00000000..4a330b8f
--- /dev/null
+++ b/app/src/main/res/drawable/motd_img.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:drawable="@drawable/background_main"
+ android:gravity="fill_horizontal|fill_vertical"/>
+
+ <item
+ android:top="50dp"
+ android:bottom="50dp"
+ android:left="50dp"
+ android:right="50dp"
+ >
+ <bitmap
+ android:src="@drawable/logo"
+ android:gravity="center" />
+ </item>
+
+</layer-list> \ No newline at end of file
diff --git a/app/src/main/res/layout/f_motd.xml b/app/src/main/res/layout/f_motd.xml
new file mode 100644
index 00000000..6e763c75
--- /dev/null
+++ b/app/src/main/res/layout/f_motd.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:theme="@style/BitmaskTheme"
+ tools:context=".base.fragments.MotdFragment"
+ android:fitsSystemWindows="true">
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline_bottom"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintBottom_toTopOf="@+id/next_btn"
+ />
+
+ <!-- The primary full-screen view. This can be replaced with whatever view
+ is needed to present your content, e.g. VideoView, SurfaceView,
+ TextureView, etc. -->
+ <ScrollView
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="@id/guideline_bottom"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ android:clipChildren="true"
+ android:fillViewport="true"
+ android:background="@drawable/background_motd"
+ >
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ >
+
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline_sv_left"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.15" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline_sv_right"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.85" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline_sv_top"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_percent="0.1" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline_sv_center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_percent="0.4" />
+
+ <de.hdodenhof.circleimageview.CircleImageView
+ android:id="@+id/motd_icon_circle"
+ android:layout_width="120dp"
+ android:layout_height="120dp"
+ app:layout_constraintTop_toTopOf="@id/guideline_sv_top"
+ app:layout_constraintBottom_toTopOf="@+id/guideline_sv_center"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ android:src="@drawable/motd_img"
+ android:visibility="visible"
+ />
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/motd_content"
+ app:layout_constraintLeft_toLeftOf="@id/guideline_sv_left"
+ app:layout_constraintRight_toRightOf="@+id/guideline_sv_right"
+ app:layout_constraintTop_toBottomOf="@+id/guideline_sv_center"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginVertical="@dimen/standard_margin"
+ android:gravity="center_horizontal"
+ android:keepScreenOn="true"
+ android:padding="@dimen/stdpadding"
+ android:elegantTextHeight="true"
+ android:autoSizeTextType="uniform"
+ android:text=" aaa aaa aaa aaaaaaaaaaaaaaaaaaaa \n\n\”ssdfsdf Riseup's services are funded by donations from people like you. We try not to ask too often, but we have to ask sometimes. Please consider making a https://riseup.net/donate donation if you value this freely available service, appreciate that we don't track or sell your data, or want to support people around the world working towards liberatory social change."
+ android:textSize="16sp"
+ android:textStyle="bold" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </ScrollView>
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@+id/next_btn"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:layout_width="match_parent"
+ android:layout_height="80dp"
+ android:layout_alignParentBottom="true"
+ android:src="@drawable/ic_arrow_right"
+ android:background="?attr/selectableItemBackground"
+ android:text="next"
+ />
+
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 792db1ee..40b51436 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -35,6 +35,7 @@
<color name="white">#ffffff</color>
<color name="white_transparent">#20ffffff</color>
<color name="black_transparent">#20000000</color>
+ <color name="black_high_transparent">#05000000</color>
<color name="colorActionBarTitleFont">@color/white</color>
<color name="colorActionBarSubtitleFont">@color/black800</color>
diff --git a/app/src/normal/assets/urls/riseup.net.url b/app/src/normal/assets/urls/riseup.net.url
index a33898c4..ad24ee99 100644
--- a/app/src/normal/assets/urls/riseup.net.url
+++ b/app/src/normal/assets/urls/riseup.net.url
@@ -2,5 +2,6 @@
"main_url" : "https://black.riseup.net",
"provider_ip" : "198.252.153.70",
"provider_api_ip" : "198.252.153.107",
- "geoip_url" : "https://api.black.riseup.net:9001/json"
+ "geoip_url" : "https://api.black.riseup.net:9001/json",
+ "motd_url": "https://static.riseup.net/vpn/motd.json"
}
diff --git a/app/src/normalProductionFatDebug/assets/urls/riseup.net.url b/app/src/normalProductionFatDebug/assets/urls/riseup.net.url
index a33898c4..ad24ee99 100644
--- a/app/src/normalProductionFatDebug/assets/urls/riseup.net.url
+++ b/app/src/normalProductionFatDebug/assets/urls/riseup.net.url
@@ -2,5 +2,6 @@
"main_url" : "https://black.riseup.net",
"provider_ip" : "198.252.153.70",
"provider_api_ip" : "198.252.153.107",
- "geoip_url" : "https://api.black.riseup.net:9001/json"
+ "geoip_url" : "https://api.black.riseup.net:9001/json",
+ "motd_url": "https://static.riseup.net/vpn/motd.json"
}
diff --git a/app/src/sharedTest/java/se.leap.bitmaskclient/testutils/TestSetupHelper.java b/app/src/sharedTest/java/se.leap.bitmaskclient/testutils/TestSetupHelper.java
index 0fdc94ca..72791ba6 100644
--- a/app/src/sharedTest/java/se.leap.bitmaskclient/testutils/TestSetupHelper.java
+++ b/app/src/sharedTest/java/se.leap.bitmaskclient/testutils/TestSetupHelper.java
@@ -84,6 +84,7 @@ public class TestSetupHelper {
Provider p = new Provider(
domain,
geoipUrl,
+ null,
providerIp,
providerApiIp,
getInputAsString(TestSetupHelper.class.getClassLoader().getResourceAsStream(caCertFile)),
diff --git a/app/src/test/java/se/leap/bitmaskclient/base/utils/DateHelperTest.java b/app/src/test/java/se/leap/bitmaskclient/base/utils/DateHelperTest.java
new file mode 100644
index 00000000..2651a64d
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/base/utils/DateHelperTest.java
@@ -0,0 +1,20 @@
+package se.leap.bitmaskclient.base.utils;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DateHelperTest {
+
+ @Test
+ public void getFormattedDateWithTimezone() {
+ Pattern datePattern = Pattern.compile("^10 Nov 22 [1]?[0-9]:26 [+-][0-9]{4}$");
+ String formattedDate = DateHelper.getFormattedDateWithTimezone(1668075969744L);
+ Matcher matcher = datePattern.matcher(formattedDate);
+ System.out.println(formattedDate);
+ assertTrue("date should be formatted similar to 10 Nov 22 11:26 +0100", matcher.find());
+ }
+} \ No newline at end of file
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java b/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
index c6ba1e6f..e14fa4c3 100644
--- a/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
@@ -12,11 +12,17 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_HASHES;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_LAST_SEEN;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_LAST_UPDATED;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PRIVATE_KEY;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.base.utils.FileHelper.createFile;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getEipDefinitionFromPreferences;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getFromPersistedProvider;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getLongFromPersistedProvider;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getStringSetFromPersistedProvider;
import android.content.Context;
import android.content.Intent;
@@ -474,11 +480,39 @@ public class MockHelper {
return providerFromPrefs.getCaCert();
case Provider.GEOIP_URL:
return providerFromPrefs.getGeoipUrl().toString();
+ case Provider.MOTD_URL:
+ return providerFromPrefs.getMotdUrl().toString();
+ case PROVIDER_MOTD:
+ return providerFromPrefs.getMotdJsonString();
}
return null;
}
});
+ when(getLongFromPersistedProvider(anyString(), anyString(), any(SharedPreferences.class))).thenAnswer(new Answer<Long>() {
+ @Override
+ public Long answer(InvocationOnMock invocation) throws Throwable {
+ String key = (String) invocation.getArguments()[0];
+ switch (key) {
+ case PROVIDER_MOTD_LAST_SEEN:
+ return providerFromPrefs.getLastMotdSeen();
+ case PROVIDER_MOTD_LAST_UPDATED:
+ return providerFromPrefs.getLastMotdUpdate();
+ }
+ return 0L;
+ }
+ });
+ when(getStringSetFromPersistedProvider(anyString(), anyString(), any(SharedPreferences.class))).thenAnswer(new Answer<Set<String>>() {
+ @Override
+ public Set<String> answer(InvocationOnMock invocation) throws Throwable {
+ String key = (String) invocation.getArguments()[0];
+ switch (key) {
+ case PROVIDER_MOTD_HASHES:
+ return providerFromPrefs.getMotdLastSeenHashes();
+ }
+ return null;
+ }
+ });
}
public static void mockPreferenceHelper(MockSharedPreferences preferences) {
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java b/app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java
index be0cf41f..4fabc2c0 100644
--- a/app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java
@@ -21,6 +21,7 @@ import android.content.SharedPreferences;
import androidx.annotation.Nullable;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@@ -48,7 +49,7 @@ public class MockSharedPreferences implements SharedPreferences {
@Nullable
@Override
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
- return null;
+ return new HashSet<>();
}
@Override
diff --git a/app/src/test/resources/v4/riseup.net.cert b/app/src/test/resources/v4/riseup.net.cert
index 86bc0b55..1beff4bd 100644
--- a/app/src/test/resources/v4/riseup.net.cert
+++ b/app/src/test/resources/v4/riseup.net.cert
@@ -1,54 +1,54 @@
-----BEGIN RSA PRIVATE KEY-----
-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
+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
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
-MIIEmzCCAoOgAwIBAgIQdYL065go8MY1TA6uhbuzrzANBgkqhkiG9w0BAQsFADB1
+MIIEmzCCAoOgAwIBAgIQejc1yqfKGehUKDMdybDH+DANBgkqhkiG9w0BAQsFADB1
MRgwFgYDVQQKDA9SaXNldXAgTmV0d29ya3MxGzAZBgNVBAsMEmh0dHBzOi8vcmlz
ZXVwLm5ldDE8MDoGA1UEAwwzUmlzZXVwIE5ldHdvcmtzIFJvb3QgQ0EgKGNsaWVu
-dCBjZXJ0aWZpY2F0ZXMgb25seSEpMB4XDTIyMDYxODAwMDAwMFoXDTIyMDkxODAw
-MDAwMFowLTErMCkGA1UEAwwiVU5MSU1JVEVENnZteWZ1MHpsdmlucmprdTN3Y2hp
-OTZsbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMLaqkm07AfyXIT6
-+NSnUsk1nxvqgcUX26mMOYcfFcc7lTT5+JxJipuCm7RIWS09ypNMiWvvV91AVZHk
-6yOiGUJrCtbNZavWE3MI7K8s2v/8bcOoDO1nfF/BWjJ2mDwQUaWllov/0bKy5s7Z
-tJElNaSW4Ei9VSbJ4BiJlj3JwD74T0uCBulw/+Ay8qIOx8oNMrmcgX+g870FDBYf
-wfUpvqIGPJxvrxGqDf0Wip9uTPrgujQtr1/vcHKi8kHodCbCANt36MslpQNrFtvm
-neFVnyNw6V8GApwriWZQ9yWaCz/DqEGnJT7ASJZfp+inlXBOx+2n9gpDYpxWvtAo
-DGLb6YECAwEAAaNvMG0wHQYDVR0OBBYEFGy9LFCeTCtyoVbQYY18zgpX0N98MAsG
+dCBjZXJ0aWZpY2F0ZXMgb25seSEpMB4XDTIyMTAxNDAwMDAwMFoXDTIzMDExNDAw
+MDAwMFowLTErMCkGA1UEAwwiVU5MSU1JVEVEMTMxaXF4cHB0cHljYnN3eGRueXRu
+aHhiMDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKkpHeVGyofh21kh
++tNzCTYEpKJngZz6KBNHZcWFyDkGvx505efuwleOyQ1oxVko9Yfs3gJl3IJEMQ0R
+da22fqKL9ukmMhop4AUIJ/ar6AmcCHXrBDeV4Xb7dtxSvHnWpwjDf7Z9Qp8mjucb
+x3Vkb4aZhH/cwLFcKPan8db5kbCX873Jx6x6AoB958cytVWZucTRkXQJvYwNqHWG
+CLkijdYUmII3J11bzR5t21rPgeEc4La2Rr8vr7EknegGmBkOLAo9wzrijTlql2fM
+haX424tK2LDps8RBYmYnqrsYa2n0jPtrsjW3T7D1yndL3mYdQKQxB1v+iPqj9G5x
+xOg7a/sCAwEAAaNvMG0wHQYDVR0OBBYEFEFB2IaA9Z5TOAPtMw3vgKvRR+tpMAsG
A1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAjAJBgNVHRMEAjAAMB8GA1Ud
-IwQYMBaAFBf0G9XlKgEBTWuiXTYKKQmWZYBGMA0GCSqGSIb3DQEBCwUAA4ICAQBy
-MezxBB08bn5L5ye3pTOvENyM0DLGdoy1N+oEHhjMW/QmxPuXzKviyxNEtFCbyZeT
-n+uQPY3zwhj4vF5PZoUXI22USISMo+bPHHo4dTfXfzln7CuPuXnUR1bFKSFGU7+B
-6TZb5qmFkWfCiZ8KE1ogKXLX2mwmnIKVpPiu07pp6msR+pxfQHBBbrzafLs5PSa8
-kTxW900eozF4KJYwcryu6A/tdkTrQqsji/NrwqJRnFqzzDV0x7iFpnp0J9xRi1oa
-nkETZXMSHkDkqL3RdTwg11RX+bnYS/GK+//z3G17iwB/rGIi9O2qN6uMhX/ZFwom
-AAqbSAzOnSU2ERaSuc7a0t9c9cNnusbYiDaNISAi7bOGs5tCbLRBwSznvIo9Dfx8
-aRnDucYp+I6Vl7xW2mM155/44laemiWn4SPRbvBgZgBWObptnuIqu9Nipwp69z/Q
-x/BBcnNu+PbdkuC9zO+OtwosK3WVkL+QiKWbT4fxXNqJV90NM1R+CteDWvixs+Bn
-xVGCH4c6+svnBmyRhDYID7ZmoeE/gnPkceh63igZ6V+w0UAnGtT9cfLK9cLawfZO
-GnO7P8YTawjRoVz9FmhUnirghq8xk0MCdGHWzT+FqGS+l270F+bbD4eUD2nQhV4J
-QJ0J3BGyzELKzFtIBIpnzPVDJDSIQkPTT/yP7gAEPg==
------END CERTIFICATE----- \ No newline at end of file
+IwQYMBaAFBf0G9XlKgEBTWuiXTYKKQmWZYBGMA0GCSqGSIb3DQEBCwUAA4ICAQB0
+x+RhbfmWuKQ7clT2oBhLOep1USJ9y98BLMsEmqZ84q0CnG+tI2QLhZvaMiGiMplI
+saHhK1lIHbO/UATHOhE+ZbY9vAaGK1JaBdlOTshjo+cijZ2nWwfwa9Qj/Cx64sSO
+cee3Lbg5IcsFc/0KGCTw0k9PheD/CudRSnLCxImA8DmJO+1w1NUoBBm48tdD6usD
+fLU35FvChvNwW/7GO/q3TGXF/jrreBnrJZMjsffwFOJC8SWZIAScmF/OrqWyruYb
+ZS9/0ZXR21TT1mvUFjIs+0uY2crfli/f91ZbB9eRoiyHkck8RtotiH9PrmYt7j02
+5cwos2BqRs+kZ7AgYvZjWErwIyCsXSzrk0WztLa2vslqZSZSt12VlW5iIyKTyx8T
+snQLEdHwzRT5C+9daq3PneOvD5KhgyySlwWSoMuJdg2n3cTs/0uMDQE7FIim11XM
+Fz8liZf4PcWv8YLFUBHQ367SvbAiyZLzpXZETQyDaYNYGTwYBCGs9Yhq3KM85Kmz
+f0rgrBpm61ujLQ/OBSCq6/RA9BN8UYdO+YS0vengYtIdc+aw1PzLhcC9dLLXC4ef
+zH25zfe7vkt58UcuG+YCsaIR8Hy3I/yDQWVkbNawEYUboEHHRd8pQmUIkHLaYibt
+eq1SXtuulUUDIkyfRATiA/GkPVPPnJImL8XK12WFPQ==
+-----END CERTIFICATE-----