diff --git a/app/src/main/java/org/fptn/vpn/services/CustomVpnService.java b/app/src/main/java/org/fptn/vpn/services/CustomVpnService.java index 29760e10..03e674e0 100644 --- a/app/src/main/java/org/fptn/vpn/services/CustomVpnService.java +++ b/app/src/main/java/org/fptn/vpn/services/CustomVpnService.java @@ -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; @@ -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; @@ -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 activeConnection = new AtomicReference<>(); private final AtomicInteger nextConnectionId = new AtomicInteger(1); @@ -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; @@ -143,16 +138,13 @@ 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) { @@ -160,9 +152,7 @@ public synchronized static void startToConnect(Context context) { 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) { @@ -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); @@ -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); @@ -272,7 +256,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { try { List 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 */ @@ -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 @@ -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() { @@ -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(); @@ -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) { @@ -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); @@ -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) @@ -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); + } } diff --git a/app/src/main/java/org/fptn/vpn/utils/NotificationUtils.java b/app/src/main/java/org/fptn/vpn/utils/NotificationUtils.java index 1f6c89b7..9549a8bc 100644 --- a/app/src/main/java/org/fptn/vpn/utils/NotificationUtils.java +++ b/app/src/main/java/org/fptn/vpn/utils/NotificationUtils.java @@ -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))); @@ -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); } } } diff --git a/app/src/main/java/org/fptn/vpn/utils/SharedPrefUtils.java b/app/src/main/java/org/fptn/vpn/utils/SharedPrefUtils.java index 1c6de962..b0985f11 100644 --- a/app/src/main/java/org/fptn/vpn/utils/SharedPrefUtils.java +++ b/app/src/main/java/org/fptn/vpn/utils/SharedPrefUtils.java @@ -30,14 +30,14 @@ public static void resetToDefaultSniHostname(Context context) { } /* NOTIFICATIONS */ - public static int getNotificationChannelVersion(Context context) { + public static int getNotificationChannelVersion(Context context, String channelVersionTag) { SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE); - return sharedPreferences.getInt(Constants.MAIN_NOTIFICATION_CHANNEL_VERSION, 0); + return sharedPreferences.getInt(channelVersionTag, 0); } - public static void saveNotificationChannelVersion(Context context, int version) { + public static void saveNotificationChannelVersion(Context context, String channelVersionTag, int version) { SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE); - sharedPreferences.edit().putInt(Constants.MAIN_NOTIFICATION_CHANNEL_VERSION, version).apply(); + sharedPreferences.edit().putInt(channelVersionTag, version).apply(); } public static boolean isPermissionsRequested(Context context) { diff --git a/app/src/main/java/org/fptn/vpn/views/HomeActivity.java b/app/src/main/java/org/fptn/vpn/views/HomeActivity.java index b96c0f02..3af3a98c 100644 --- a/app/src/main/java/org/fptn/vpn/views/HomeActivity.java +++ b/app/src/main/java/org/fptn/vpn/views/HomeActivity.java @@ -183,6 +183,7 @@ private void initializeVariable() { disconnectedStateUiItems(); break; default: + break; } diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index b8e16a78..b2acee26 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -75,8 +75,10 @@ https://play.google.com/store/apps/details?id=org.fptn.vpn Когда подключено Основные + Ошибки Отключить + Переподключиться Токен был обновлен! Неверный формат токена! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d643c079..97f21034 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -78,7 +78,9 @@ https://play.google.com/store/apps/details?id=org.fptn.vpn When connected Main + Errors Disconnect + Reconnect Token was updated! Invalid link format! diff --git a/core/common/src/main/kotlin/org/fptn/vpn/core/common/Constants.kt b/core/common/src/main/kotlin/org/fptn/vpn/core/common/Constants.kt index 817a7bfb..c4281dc3 100644 --- a/core/common/src/main/kotlin/org/fptn/vpn/core/common/Constants.kt +++ b/core/common/src/main/kotlin/org/fptn/vpn/core/common/Constants.kt @@ -15,10 +15,14 @@ object Constants { const val MAIN_CONNECTED_NOTIFICATION_ID = 8975 const val INFO_NOTIFICATION_NOTIFICATION_ID = 8979 + const val ERROR_NOTIFICATION_CHANNEL_ID = "fptnvpn-notification-error" + const val ERROR_NOTIFICATION_CHANNEL_VERSION = "fptnvpn-notification-error-channel-version" + const val ERROR_NOTIFICATION_CHANNEL_VERSION_NUM = 1 + const val ERROR_NOTIFICATION_CHANNEL_GROUP_ID = "fptnvpn-notification-error-group" + const val ERROR_CONNECTED_NOTIFICATION_ID = 8989 + // Shares preferences constants const val CURRENT_SNI_SHARED_PREF_KEY: String = "CURRENT_SNI" - const val AUTO_SNI_ENABLED_PREF_KEY: String = "AUTO_SNI_ENABLED" - const val RESET_SELECTED_SERVER_PREF_KEY: String = "RESET_SELECTED_SERVER_PREF_KEY" const val APPLICATION_SHARED_PREFERENCES = "fptnvpn-shared-preferences" const val PERMISSIONS_REQUESTED_SHARED_PREF_KEY: String = "permissions_requested_previously"