Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 66 additions & 99 deletions app/src/main/java/org/fptn/vpn/services/CustomVpnService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import static org.fptn.vpn.core.common.Constants.SELECTED_SERVER;
import static org.fptn.vpn.core.common.Constants.SELECTED_SERVER_ID_AUTO;
import static org.fptn.vpn.core.common.Constants.START_FROM_TILE_AUTO;
import static org.fptn.vpn.utils.ResourcesUtils.getStringResourceByName;

import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
Expand All @@ -22,7 +22,6 @@
import android.os.Build;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.SystemClock;
import android.service.quicksettings.TileService;
import android.util.Log;

Expand Down Expand Up @@ -64,13 +63,8 @@ public class CustomVpnService extends VpnService {
public static final String ACTION_CONNECT = "CustomVpnService:CONNECT";
public static final String ACTION_DISCONNECT = "CustomVpnService:DISCONNECT";
public static final String ACTION_BIND = "CustomVpnService:BIND";

public static final String FPTN_SERVICE_POWER_LOCK = "CustomVpnService::POWER_LOCK";

// for set alarming
public static final String ACTION_RESET_ALARM = "CustomVpnService:ACTION_RESET_ALARM";
public static final int ONE_MINUTE = 60_000;

private final AtomicReference<CustomVpnConnection> activeConnection = new AtomicReference<>();

private final AtomicInteger nextConnectionId = new AtomicInteger(1);
Expand All @@ -81,9 +75,10 @@ public class CustomVpnService extends VpnService {
// Pending Intent to disconnect from notification
private PendingIntent disconnectPendingIntent;

private FptnServerRepository fptnServerRepository;
// Pending Intent to reconnect from notification
private PendingIntent reconnectPendingIntent;

private boolean isNotificationAllowed = false;
private FptnServerRepository fptnServerRepository;

private ConnectivityManager.NetworkCallback networkCallback;
private ConnectivityManager connectivityManager;
Expand Down Expand Up @@ -143,26 +138,21 @@ public IBinder onBind(Intent intent) {
}

/* Static methods to start/stop service */

public synchronized static void startToConnect(Context context, FptnServerDto fptnServerDto) {
Intent intent = new Intent(context, CustomVpnService.class);
intent.setAction(ACTION_CONNECT);
if (fptnServerDto != null) {
intent.putExtra(SELECTED_SERVER, fptnServerDto.id);
}

// If started service not become foreground - will be exception ANR - after 5 seconds
context.startForegroundService(intent);
context.startService(intent);
}

public synchronized static void startToConnect(Context context) {
Intent intent = new Intent(context, CustomVpnService.class);
intent.setAction(ACTION_CONNECT);
// Now it method called only from FptnTileService
intent.putExtra(SELECTED_SERVER, START_FROM_TILE_AUTO);

// If started service not become foreground - will be exception ANR - after 5 seconds
context.startForegroundService(intent);
context.startService(intent);
}

public synchronized static void startToDisconnect(Context context) {
Expand All @@ -174,20 +164,26 @@ public synchronized static void startToDisconnect(Context context) {
@Override
public void onCreate() {
Log.i(TAG, "CustomVpnService.onCreate() Thread.Id: " + Thread.currentThread().getId());

// pending intent for open MainActivity on tap
launchMainActivityPendingIntent = PendingIntent.getActivity(this, 0,
new Intent(this, HomeActivity.class),
PendingIntent.FLAG_IMMUTABLE);
// pending intent for disconnect button in notification

// pending intent for disconnect button in connected notification
disconnectPendingIntent = PendingIntent.getService(this, 0,
new Intent(this, CustomVpnService.class)
.setAction(CustomVpnService.ACTION_DISCONNECT),
PendingIntent.FLAG_IMMUTABLE);
fptnServerRepository = new FptnServerRepository(getApplicationContext());

/* check if notification allowed */
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
isNotificationAllowed = notificationManager.areNotificationsEnabled();
// pending intent for reconnect button in error notification
reconnectPendingIntent = PendingIntent.getService(this, 0,
new Intent(this, CustomVpnService.class)
.setAction(CustomVpnService.ACTION_CONNECT),
PendingIntent.FLAG_IMMUTABLE);

// create fptnServerRepository
fptnServerRepository = new FptnServerRepository(getApplicationContext());

connectivityManager = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);

Expand All @@ -205,43 +201,31 @@ public void onCreate() {
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "CustomVpnService.onStartCommand() Intent: " + intent + ", Thread.Id: " + Thread.currentThread().getId());

if (intent != null && ACTION_RESET_ALARM.equals(intent.getAction())) {
resetServiceKeepAliveAlarm();
return START_STICKY;
}

executorService.submit(() -> {
/* check is internet connection available */
if (!NetworkUtils.isOnline(connectivityManager)) {
Log.e(TAG, "onStartCommand: no active internet connections!");
disconnect(new PVNClientException(ErrorCode.NO_ACTIVE_INTERNET_CONNECTIONS));
return;
}

try {
/* check is internet connection available */
if (!NetworkUtils.isOnline(connectivityManager)) {
Log.e(TAG, "onStartCommand: no active internet connections!");
disconnect(new PVNClientException(ErrorCode.NO_ACTIVE_INTERNET_CONNECTIONS));
return;
}

// dismiss error notification
NotificationManager notificationManager = (NotificationManager) getSystemService(
NOTIFICATION_SERVICE);
notificationManager.cancel(Constants.ERROR_CONNECTED_NOTIFICATION_ID);

String sniHostname = SharedPrefUtils.getSniHostname(getApplicationContext());
BypassCensorshipMethod bypassCensorshipMethod = SharedPrefUtils.getBypassCensorshipMethod(this);

if (intent == null) {
/* restart after service destruction - all fields of intent is null */
Log.w(TAG, "onStartCommand: restart after service was killed");
FptnServerDto server = fptnServerRepository.getSelected().get();
if (server != null) {
connect(server, sniHostname);
} else {
/* selected server not selected - that should means that we were disconnected correct previously */
Log.i(TAG, "connectToPreviouslySelectedServer: previously selected server is null. No need to reconnect");
disconnect();
}
} else if (ACTION_DISCONNECT.equals(intent.getAction())) {
Log.i(TAG, "onStartCommand: disconnect!");
/* if we need disconnect */
boolean isActiveState = serviceStateMutableLiveData.getValue().getConnectionState().isActiveState();
if (ACTION_DISCONNECT.equals(intent.getAction()) && isActiveState) {
if (SharedPrefUtils.getResetSelectedServerEnabled(this)) {
fptnServerRepository.resetSelected();
fptnServerRepository.resetSelected().get();
}
// stop running threads
disconnect();
} else if (ACTION_CONNECT.equals(intent.getAction())) {
} else if (ACTION_CONNECT.equals(intent.getAction()) && !isActiveState) {
setConnectionState(ConnectionState.CONNECTING, null);

int serverId = intent.getIntExtra(SELECTED_SERVER, SELECTED_SERVER_ID_AUTO);
Expand Down Expand Up @@ -272,7 +256,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
try {
List<FptnServerDto> fptnServerDtos = fptnServerRepository.getServersListFuture(false).get();
FptnServerDto server = SpeedTestUtils.findFastestServer(fptnServerDtos, sniHostname, bypassCensorshipMethod);
fptnServerRepository.setIsSelected(server.id);
fptnServerRepository.setIsSelected(server.id).get();
connect(server, sniHostname);
} catch (PVNClientException e) {
/* We don't need to connect if all servers are unreachable */
Expand All @@ -285,15 +269,13 @@ public int onStartCommand(Intent intent, int flags, int startId) {
FptnServerDto server = fptnServerRepository.getSelected().get();
connect(server, sniHostname);
}

// for infinite run
resetServiceKeepAliveAlarm();
}
} catch (ExecutionException | InterruptedException e) {
} catch (ExecutionException | InterruptedException | RuntimeException e) {
disconnect(new PVNClientException(e.getMessage()));
}
});
return START_STICKY;

return START_NOT_STICKY;
}

@Override
Expand All @@ -308,34 +290,6 @@ public void onDestroy() {
}
}

// this methods call when user remove activity from stack
@Override
public void onTaskRemoved(Intent rootIntent) {
Log.i(TAG, "onTaskRemoved()");
}

// alarming must keep service alive (maybe not)
private void resetServiceKeepAliveAlarm() {
Log.i(TAG, "resetAlarm()");

ConnectionState connectionState = Optional.ofNullable(serviceStateMutableLiveData.getValue()).map(CustomVpnServiceState::getConnectionState).orElse(null);
if (connectionState == ConnectionState.CONNECTED ||
connectionState == ConnectionState.CONNECTING ||
connectionState == ConnectionState.RECONNECTING) {
Log.i(TAG, "Add restart event with one minute delay");
AlarmManager systemService = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
if (systemService != null) {
Intent restartIntent = new Intent(getApplicationContext(), CustomVpnService.class);
restartIntent.setPackage(getPackageName());
restartIntent.setAction(CustomVpnService.ACTION_RESET_ALARM);

PendingIntent pendingRestartIntent = PendingIntent.getService(getApplicationContext(), 1, restartIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
systemService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + ONE_MINUTE, pendingRestartIntent);
}
} else {
Log.i(TAG, "No need keep alive");
}
}

private void registerNetworkCallback() {
networkCallback = new ConnectivityManager.NetworkCallback() {
Expand Down Expand Up @@ -525,6 +479,16 @@ private void disconnect(PVNClientException exception) {
//send to UI activity that state is disconnected.
setConnectionState(ConnectionState.DISCONNECTED, exception);

if (exception != null) {
ErrorCode errorCode = exception.errorCode;
if (errorCode != ErrorCode.UNKNOWN_ERROR) {
String stringResourceByName = getStringResourceByName(getApplication(), errorCode.getValue());
showErrorNotification(stringResourceByName);
} else {
showErrorNotification(exception.errorMessage);
}
}

// Release wakelock
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
Expand All @@ -539,19 +503,11 @@ private void disconnect(PVNClientException exception) {
}

private void removeForegroundNotification() {
if (!isNotificationAllowed) {
return;
}

NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(Constants.MAIN_CONNECTED_NOTIFICATION_ID);
}

private void startForegroundWithNotification(String title) {
if (!isNotificationAllowed) {
return;
}

NotificationUtils.configureNotificationChannel(this);
Notification notification = createNotification(title, "");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
Expand All @@ -562,9 +518,6 @@ private void startForegroundWithNotification(String title) {
}

private void updateNotificationWithMessage(String title, String message) {
if (!isNotificationAllowed) {
return;
}
NotificationManager notificationManager = (NotificationManager) getSystemService(
NOTIFICATION_SERVICE);
Notification notification = createNotification(title, message);
Expand All @@ -575,7 +528,6 @@ private Notification createNotification(String title, String message) {
// In Api level 24 an above, there is no icon in design!!!
Notification.Action actionDisconnect = new Notification.Action.Builder(null, getString(R.string.disconnect_action), disconnectPendingIntent)
.build();
// todo: add log if service in foreground?
Notification.Builder builder = new Notification.Builder(this, Constants.MAIN_NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_logo)
.setContentTitle(title)
Expand All @@ -593,19 +545,34 @@ private Notification createNotification(String title, String message) {
}

private void showReconnectionFailedNotification() {
if (!isNotificationAllowed) {
return;
}

Notification notification = new Notification.Builder(this, Constants.MAIN_NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_logo)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setContentTitle(getApplication().getString(R.string.reconnecting_failed))
.setContentIntent(launchMainActivityPendingIntent)
.setAutoCancel(true) // if you tap on notification - opens activity and notification dismissed
.build();

NotificationManager notificationManager = (NotificationManager) getSystemService(
NOTIFICATION_SERVICE);
notificationManager.notify(Constants.INFO_NOTIFICATION_NOTIFICATION_ID, notification);
}

private void showErrorNotification(String message) {
Notification.Action actionDisconnect = new Notification.Action.Builder(null, getString(R.string.reconnect_action), reconnectPendingIntent)
.build();
Notification notification = new Notification.Builder(this, Constants.ERROR_NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_logo)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setContentTitle(getApplication().getString(R.string.error))
.setContentText(message)
.setAutoCancel(true) // if you tap on notification - opens activity and notification dismissed
.setContentIntent(launchMainActivityPendingIntent)
.addAction(actionDisconnect)
.build();

NotificationManager notificationManager = (NotificationManager) getSystemService(
NOTIFICATION_SERVICE);
notificationManager.notify(Constants.ERROR_CONNECTED_NOTIFICATION_ID, notification);
}
}
38 changes: 31 additions & 7 deletions app/src/main/java/org/fptn/vpn/utils/NotificationUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@
import org.fptn.vpn.core.common.Constants;

public class NotificationUtils {

public static void configureNotificationChannel(Context context) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel notificationChannel = notificationManager.getNotificationChannel(Constants.MAIN_NOTIFICATION_CHANNEL_ID);

// add main notification channel
NotificationChannel mainNotificationChannel = notificationManager.getNotificationChannel(Constants.MAIN_NOTIFICATION_CHANNEL_ID);
int mainNotificationChannelOnDevice = SharedPrefUtils.getNotificationChannelVersion(context, Constants.MAIN_NOTIFICATION_CHANNEL_VERSION);
// remove existed notification channel if their version lower than in constants
int notificationChannelOnDevice = SharedPrefUtils.getNotificationChannelVersion(context);
if (notificationChannel != null && notificationChannelOnDevice < Constants.MAIN_NOTIFICATION_CHANNEL_VERSION_NUM) {
if (mainNotificationChannel != null && mainNotificationChannelOnDevice < Constants.MAIN_NOTIFICATION_CHANNEL_VERSION_NUM) {
notificationManager.deleteNotificationChannel(Constants.MAIN_NOTIFICATION_CHANNEL_ID);
notificationChannel = null;
mainNotificationChannel = null;
}

if (notificationChannel == null) {
if (mainNotificationChannel == null) {
notificationManager.createNotificationChannelGroup(
new NotificationChannelGroup(Constants.MAIN_NOTIFICATION_CHANNEL_GROUP_ID, context.getString(R.string.notification_group_name)));

Expand All @@ -32,7 +33,30 @@ public static void configureNotificationChannel(Context context) {
newNotificationChannel.setSound(null, null); //disable sound

notificationManager.createNotificationChannel(newNotificationChannel);
SharedPrefUtils.saveNotificationChannelVersion(context, Constants.MAIN_NOTIFICATION_CHANNEL_VERSION_NUM);
SharedPrefUtils.saveNotificationChannelVersion(context, Constants.MAIN_NOTIFICATION_CHANNEL_VERSION, Constants.MAIN_NOTIFICATION_CHANNEL_VERSION_NUM);
}

// add error notification channel
NotificationChannel errorNotificationChannel = notificationManager.getNotificationChannel(Constants.ERROR_NOTIFICATION_CHANNEL_ID);
int errorNotificationChannelOnDevice = SharedPrefUtils.getNotificationChannelVersion(context, Constants.ERROR_NOTIFICATION_CHANNEL_VERSION);
// remove existed notification channel if their version lower than in constants
if (errorNotificationChannel != null && errorNotificationChannelOnDevice < Constants.ERROR_NOTIFICATION_CHANNEL_VERSION_NUM) {
notificationManager.deleteNotificationChannel(Constants.ERROR_NOTIFICATION_CHANNEL_ID);
errorNotificationChannel = null;
}

if (errorNotificationChannel == null) {
notificationManager.createNotificationChannelGroup(
new NotificationChannelGroup(Constants.ERROR_NOTIFICATION_CHANNEL_GROUP_ID, context.getString(R.string.errors_notification_group_name)));

NotificationChannel newNotificationChannel = new NotificationChannel(
Constants.ERROR_NOTIFICATION_CHANNEL_ID,
context.getString(R.string.errors_notification_group_name),
NotificationManager.IMPORTANCE_HIGH);
newNotificationChannel.setGroup(Constants.ERROR_NOTIFICATION_CHANNEL_GROUP_ID);

notificationManager.createNotificationChannel(newNotificationChannel);
SharedPrefUtils.saveNotificationChannelVersion(context, Constants.ERROR_NOTIFICATION_CHANNEL_VERSION, Constants.ERROR_NOTIFICATION_CHANNEL_VERSION_NUM);
}
}
}
Loading