From fc1a1144198de08b8e77b98bb7777ad36b7f6d4f Mon Sep 17 00:00:00 2001 From: DX Date: Sat, 10 Aug 2024 20:57:49 +0330 Subject: [PATCH] Ready to release V5 (#375) Fixed Hot issue bugs related to Android API. Add enhanced logging logic. Add support IPV4/6 endpoint. Add support multi-endpoint. Improved performance. Add Turkish Language. --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 20 +- .../oblivion/BypassListAppsAdapter.java | 70 ++- .../java/org/bepass/oblivion/EditSheet.java | 5 +- .../bepass/oblivion/EndpointsBottomSheet.java | 128 ++++++ .../oblivion/SplitTunnelOptionsAdapter.java | 9 +- .../oblivion/base/StateAwareBaseActivity.java | 36 +- .../oblivion/enums/SplitTunnelMode.java | 4 +- .../oblivion/service/OblivionVpnService.java | 400 ++++++++++-------- .../org/bepass/oblivion/ui/InfoActivity.java | 58 +-- .../org/bepass/oblivion/ui/LogActivity.java | 87 +++- .../org/bepass/oblivion/ui/MainActivity.java | 181 +++----- .../bepass/oblivion/ui/SettingsActivity.java | 167 +++++--- .../oblivion/ui/SplashScreenActivity.java | 5 +- .../oblivion/ui/SplitTunnelActivity.java | 6 +- .../oblivion/utils/BatteryOptimization.kt | 55 ++- .../bepass/oblivion/utils/CountryUtils.java | 46 +- .../bepass/oblivion/utils/FileManager.java | 360 ++++++---------- .../org/bepass/oblivion/utils/ISPUtils.kt | 55 +++ .../oblivion/utils/LocalController.java | 91 ++-- .../bepass/oblivion/utils/LocaleHandler.java | 5 +- .../bepass/oblivion/utils/LocaleHelper.java | 8 +- .../bepass/oblivion/utils/NetworkUtils.java | 56 +++ .../bepass/oblivion/utils/PublicIPUtils.java | 9 +- .../bepass/oblivion/utils/SystemUtils.java | 6 +- .../bepass/oblivion/utils/ThemeHelper.java | 65 ++- app/src/main/res/drawable/check.xml | 5 + .../res/drawable/custom_progress_drawable.xml | 25 ++ app/src/main/res/drawable/expand_up.xml | 9 + .../main/res/drawable/toast_background.xml | 6 + app/src/main/res/layout/activity_info.xml | 49 ++- app/src/main/res/layout/activity_log.xml | 47 +- app/src/main/res/layout/activity_main.xml | 27 +- app/src/main/res/layout/activity_settings.xml | 246 +++++++++-- .../res/layout/activity_splash_screen.xml | 3 +- .../main/res/layout/activity_split_tunnel.xml | 23 +- .../res/layout/bottom_sheet_endpoints.xml | 111 +++++ .../layout/dialog_battery_optimization.xml | 121 +++--- app/src/main/res/layout/edit_sheet.xml | 13 +- .../main/res/layout/installed_app_item.xml | 29 +- app/src/main/res/layout/item_endpoint.xml | 41 ++ .../main/res/layout/split_tunnel_options.xml | 24 +- app/src/main/res/layout/toast.xml | 27 ++ app/src/main/res/values-fa/strings.xml | 17 +- app/src/main/res/values-ru/strings.xml | 19 +- app/src/main/res/values-tr/strings.xml | 95 +++++ app/src/main/res/values-zh/strings.xml | 9 + app/src/main/res/values/strings.xml | 255 +++++------ app/src/main/res/values/styles.xml | 6 + app/src/main/res/values/themes.xml | 12 + .../main/res/xml/network_security_config.xml | 6 + 51 files changed, 2025 insertions(+), 1133 deletions(-) create mode 100644 app/src/main/java/org/bepass/oblivion/EndpointsBottomSheet.java create mode 100644 app/src/main/java/org/bepass/oblivion/utils/ISPUtils.kt create mode 100644 app/src/main/java/org/bepass/oblivion/utils/NetworkUtils.java create mode 100644 app/src/main/res/drawable/check.xml create mode 100644 app/src/main/res/drawable/custom_progress_drawable.xml create mode 100644 app/src/main/res/drawable/expand_up.xml create mode 100644 app/src/main/res/drawable/toast_background.xml create mode 100644 app/src/main/res/layout/bottom_sheet_endpoints.xml create mode 100644 app/src/main/res/layout/item_endpoint.xml create mode 100644 app/src/main/res/layout/toast.xml create mode 100644 app/src/main/res/values-tr/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/network_security_config.xml diff --git a/app/build.gradle b/app/build.gradle index 7dc19403..898acc1c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -63,6 +63,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.vdurmont:emoji-java:5.1.1' implementation 'com.github.erfansn:locale-config-x:1.0.1' + implementation 'com.tencent:mmkv:1.3.7' implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar']) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b00da569..a36a1bae 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,22 +8,17 @@ - - - - - - + + + + @@ -50,6 +46,7 @@ android:process=":vpn_background"> + - + @@ -95,17 +92,14 @@ - - - { + private final ExecutorService executor = Executors.newSingleThreadExecutor(); private final Handler handler = new Handler(Looper.getMainLooper()); - private final FileManager fm; private final LoadListener loadListener; private List appList = new ArrayList<>(); private OnAppSelectListener onAppSelectListener; - public BypassListAppsAdapter(Context context, LoadListener loadListener) { - fm = FileManager.getInstance(context); this.loadListener = loadListener; - if (loadListener != null) - loadListener.onLoad(true); + loadApps(context, false); + } + + private void loadApps(Context context, boolean shouldShowSystemApps) { + if (loadListener != null) loadListener.onLoad(true); executor.submit(() -> { - //Querying installed apps is pretty expensive. Offload it to a worker thread. - this.appList = getInstalledApps(context, false); - //Post the result to the main looper. - handler.post(this::notifyDataSetChanged); + appList = getInstalledApps(context, shouldShowSystemApps); handler.post(() -> { - if (loadListener != null) - loadListener.onLoad(false); + notifyDataSetChanged(); + if (loadListener != null) loadListener.onLoad(false); }); }); - } - - private static List getInstalledApps(Context context, boolean shouldShowSystemApps) { - FileManager fm = FileManager.getInstance(context); - Set selectedApps = fm.getStringSet("splitTunnelApps", new HashSet<>()); - - final PackageManager pm = context.getPackageManager(); - List packages = pm.getInstalledApplications(PackageManager.GET_META_DATA); + private List getInstalledApps(Context context, boolean shouldShowSystemApps) { + Set selectedApps = FileManager.getStringSet("splitTunnelApps", new HashSet<>()); + PackageManager packageManager = context.getPackageManager(); + @SuppressLint("QueryPermissionsNeeded") List packages = packageManager.getInstalledApplications(PackageManager.GET_META_DATA); List appList = new ArrayList<>(packages.size()); + for (ApplicationInfo packageInfo : packages) { - if ((packageInfo.flags & ApplicationInfo.FLAG_SYSTEM) == ApplicationInfo.FLAG_SYSTEM && !shouldShowSystemApps) - continue; - if (packageInfo.packageName.equals(context.getPackageName())) - continue; + if (!shouldShowSystemApps && (packageInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) continue; + if (packageInfo.packageName.equals(context.getPackageName())) continue; + appList.add(new AppInfo( - packageInfo.loadLabel(pm).toString(), - () -> packageInfo.loadIcon(pm), + packageInfo.loadLabel(packageManager).toString(), + () -> packageInfo.loadIcon(packageManager), packageInfo.packageName, selectedApps.contains(packageInfo.packageName) )); @@ -78,15 +73,7 @@ private static List getInstalledApps(Context context, boolean shouldSho } public void setShouldShowSystemApps(Context context, boolean shouldShowSystemApps) { - if (loadListener != null) loadListener.onLoad(true); - executor.submit(() -> { - appList = getInstalledApps(context, shouldShowSystemApps); - handler.post(this::notifyDataSetChanged); - handler.post(() -> { - if (loadListener != null) loadListener.onLoad(false); - }); - }); - + loadApps(context, shouldShowSystemApps); } public void setOnAppSelectListener(OnAppSelectListener onAppSelectListener) { @@ -110,19 +97,20 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.itemView.setOnClickListener(v -> { appInfo.isSelected = !appInfo.isSelected; notifyItemChanged(position); - Set newSet = new HashSet<>(fm.getStringSet("splitTunnelApps", new HashSet<>())); + + Set newSet = new HashSet<>(FileManager.getStringSet("splitTunnelApps", new HashSet<>())); if (appInfo.isSelected) { newSet.add(appInfo.packageName); } else { newSet.remove(appInfo.packageName); } - fm.set("splitTunnelApps", newSet); + FileManager.set("splitTunnelApps", newSet); + if (onAppSelectListener != null) onAppSelectListener.onSelect(appInfo.packageName, appInfo.isSelected); }); } - @Override public int getItemCount() { return appList.size(); @@ -155,15 +143,15 @@ public static class AppInfo { IconLoader iconLoader; boolean isSelected; - AppInfo(String name, IconLoader iconLoader, String packageName, boolean isSelected) { - this.appName = name; + AppInfo(String appName, IconLoader iconLoader, String packageName, boolean isSelected) { + this.appName = appName; this.packageName = packageName; this.iconLoader = iconLoader; this.isSelected = isSelected; } - private interface IconLoader { + interface IconLoader { Drawable load(); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/bepass/oblivion/EditSheet.java b/app/src/main/java/org/bepass/oblivion/EditSheet.java index 4e7f5e77..1d2a8c2b 100644 --- a/app/src/main/java/org/bepass/oblivion/EditSheet.java +++ b/app/src/main/java/org/bepass/oblivion/EditSheet.java @@ -28,7 +28,6 @@ public class EditSheet { public EditSheet(Context context, String title, String sharedPrefKey, SheetsCallBack sheetsCallBack) { this.context = context; - fileManager = FileManager.getInstance(context); this.title = context.getString(R.string.editSheetEndpoint).replace("Endpoint",title); this.sharedPrefKey = sharedPrefKey; @@ -61,11 +60,11 @@ public void start() { } titleView.setText(title); - value.setText(fileManager.getString("USERSETTING_" + sharedPrefKey)); + value.setText(FileManager.getString("USERSETTING_" + sharedPrefKey)); cancel.setOnClickListener(v -> sheet.cancel()); apply.setOnClickListener(v -> { - fileManager.set("USERSETTING_" + sharedPrefKey, value.getText().toString()); + FileManager.set("USERSETTING_" + sharedPrefKey, value.getText().toString()); sheet.cancel(); }); diff --git a/app/src/main/java/org/bepass/oblivion/EndpointsBottomSheet.java b/app/src/main/java/org/bepass/oblivion/EndpointsBottomSheet.java new file mode 100644 index 00000000..ab61862b --- /dev/null +++ b/app/src/main/java/org/bepass/oblivion/EndpointsBottomSheet.java @@ -0,0 +1,128 @@ +package org.bepass.oblivion; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; + +import org.bepass.oblivion.utils.FileManager; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class EndpointsBottomSheet extends BottomSheetDialogFragment { + private List endpointsList; + public EndpointSelectionListener selectionListener; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.bottom_sheet_endpoints, container, false); + + RecyclerView recyclerView = view.findViewById(R.id.recyclerView); + endpointsList = new ArrayList<>(); + loadEndpoints(); + + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + EndpointsAdapter adapter = new EndpointsAdapter(endpointsList, this::onEndpointSelected); + recyclerView.setAdapter(adapter); + + return view; + } + + private void loadEndpoints() { + Set savedEndpoints = FileManager.getStringSet("saved_endpoints", new HashSet<>()); + for (String endpoint : savedEndpoints) { + String[] parts = endpoint.split("::"); + if (parts.length == 2) { + endpointsList.add(new Endpoint(parts[0], parts[1])); + } + } + } + + private void onEndpointSelected(String content) { + if (selectionListener != null) { + selectionListener.onEndpointSelected(content); + } + dismiss(); // Close the bottom sheet after selection + } + + public void setEndpointSelectionListener(EndpointSelectionListener listener) { + this.selectionListener = listener; + } + + private static class Endpoint { + private final String title; + private final String content; + + Endpoint(String title, String content) { + this.title = title; + this.content = content; + } + + public String getTitle() { + return title; + } + + public String getContent() { + return content; + } + } + + private static class EndpointsAdapter extends RecyclerView.Adapter { + private final List endpointsList; + public final EndpointSelectionListener selectionListener; + + EndpointsAdapter(List endpointsList, EndpointSelectionListener selectionListener) { + this.endpointsList = endpointsList; + this.selectionListener = selectionListener; + } + + @NonNull + @Override + public EndpointViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_endpoint, parent, false); + return new EndpointViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull EndpointViewHolder holder, int position) { + Endpoint endpoint = endpointsList.get(position); + holder.titleTextView.setText(endpoint.getTitle()); + holder.contentTextView.setText(endpoint.getContent()); + + holder.itemView.setOnClickListener(v -> { + if (selectionListener != null) { + selectionListener.onEndpointSelected(endpoint.getContent()); + } + }); + } + + @Override + public int getItemCount() { + return endpointsList.size(); + } + + static class EndpointViewHolder extends RecyclerView.ViewHolder { + TextView titleTextView, contentTextView; + + EndpointViewHolder(@NonNull View itemView) { + super(itemView); + titleTextView = itemView.findViewById(R.id.titleTextView); + contentTextView = itemView.findViewById(R.id.contentTextView); + } + } + } + + public interface EndpointSelectionListener { + void onEndpointSelected(String content); + } +} diff --git a/app/src/main/java/org/bepass/oblivion/SplitTunnelOptionsAdapter.java b/app/src/main/java/org/bepass/oblivion/SplitTunnelOptionsAdapter.java index 83288c15..701e6912 100644 --- a/app/src/main/java/org/bepass/oblivion/SplitTunnelOptionsAdapter.java +++ b/app/src/main/java/org/bepass/oblivion/SplitTunnelOptionsAdapter.java @@ -18,12 +18,9 @@ public class SplitTunnelOptionsAdapter extends RecyclerView.Adapter { if (isChecked) { settingsCallback.splitTunnelMode(SplitTunnelMode.DISABLED); - fm.set("splitTunnelMode", SplitTunnelMode.DISABLED.toString()); + FileManager.set("splitTunnelMode", SplitTunnelMode.DISABLED.toString()); } }); holder.blacklist.setOnCheckedChangeListener((buttonView, isChecked) -> { if (isChecked) { settingsCallback.splitTunnelMode(SplitTunnelMode.BLACKLIST); - fm.set("splitTunnelMode", SplitTunnelMode.BLACKLIST.toString()); + FileManager.set("splitTunnelMode", SplitTunnelMode.BLACKLIST.toString()); } }); diff --git a/app/src/main/java/org/bepass/oblivion/base/StateAwareBaseActivity.java b/app/src/main/java/org/bepass/oblivion/base/StateAwareBaseActivity.java index 76bcd7c2..547caa02 100644 --- a/app/src/main/java/org/bepass/oblivion/base/StateAwareBaseActivity.java +++ b/app/src/main/java/org/bepass/oblivion/base/StateAwareBaseActivity.java @@ -8,6 +8,7 @@ import android.os.Bundle; import android.os.IBinder; import android.os.Messenger; +import android.util.Log; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; @@ -19,11 +20,12 @@ import org.bepass.oblivion.utils.ColorUtils; import org.bepass.oblivion.utils.SystemUtils; - /** - * Those activities that inherit this class observe connection state by default and have access to lastKnownConnectionState variable. + * Activities inheriting from this class observe connection state by default and have access to lastKnownConnectionState variable. */ -public abstract class StateAwareBaseActivity extends AppCompatActivity { +public abstract class StateAwareBaseActivity extends AppCompatActivity { + private static final String TAG = "StateAwareBaseActivity"; + protected ConnectionState lastKnownConnectionState = ConnectionState.DISCONNECTED; private static boolean requireRestartVpnService = false; protected B binding; @@ -34,15 +36,9 @@ public abstract class StateAwareBaseActivity extends protected abstract int getStatusBarColor(); - /** - * Called when the activity is starting. - * @param savedInstanceState If the activity is being re-initialized after previously being shut down then this Bundle contains the data it most recently supplied in onSaveInstanceState(Bundle). - * @see AppCompatActivity#onCreate(Bundle) - */ @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Inflates the layout and initializes the binding object binding = DataBindingUtil.setContentView(this, getLayoutResourceId()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { SystemUtils.setStatusBarColor( @@ -55,8 +51,8 @@ public static boolean getRequireRestartVpnService() { return requireRestartVpnService; } - public static void setRequireRestartVpnService(boolean b) { - StateAwareBaseActivity.requireRestartVpnService = b; + public static void setRequireRestartVpnService(boolean b) { + StateAwareBaseActivity.requireRestartVpnService = b; } private final ServiceConnection connection = new ServiceConnection() { @@ -78,8 +74,12 @@ public void onServiceDisconnected(ComponentName arg0) { protected abstract void onConnectionStateChange(ConnectionState state); - private void observeConnectionStatus() { - if (!isBound) return; + public void observeConnectionStatus() { + if (!isBound || serviceMessenger == null) { + Log.w(TAG, "Service is not bound or messenger is null"); + return; + } + OblivionVpnService.registerConnectionStateObserver(getKey(), serviceMessenger, state -> { if (lastKnownConnectionState == state) return; lastKnownConnectionState = state; @@ -88,25 +88,27 @@ private void observeConnectionStatus() { } private void unsubscribeConnectionStatus() { - if (!isBound) return; + if (!isBound || serviceMessenger == null) { + Log.w(TAG, "Service is not bound or messenger is null"); + return; + } + OblivionVpnService.unregisterConnectionStateObserver(getKey(), serviceMessenger); } @Override protected void onStart() { super.onStart(); - // Bind to the service bindService(new Intent(this, OblivionVpnService.class), connection, Context.BIND_AUTO_CREATE); } @Override protected void onStop() { super.onStop(); - // Unbind from the service if (isBound) { unsubscribeConnectionStatus(); unbindService(connection); isBound = false; } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/bepass/oblivion/enums/SplitTunnelMode.java b/app/src/main/java/org/bepass/oblivion/enums/SplitTunnelMode.java index 8d6d8833..c674e131 100644 --- a/app/src/main/java/org/bepass/oblivion/enums/SplitTunnelMode.java +++ b/app/src/main/java/org/bepass/oblivion/enums/SplitTunnelMode.java @@ -6,10 +6,10 @@ public enum SplitTunnelMode { DISABLED, BLACKLIST; - public static SplitTunnelMode getSplitTunnelMode(FileManager fm) { + public static SplitTunnelMode getSplitTunnelMode() { SplitTunnelMode splitTunnelMode; try { - splitTunnelMode = SplitTunnelMode.valueOf(fm.getString("splitTunnelMode", SplitTunnelMode.DISABLED.toString())); + splitTunnelMode = SplitTunnelMode.valueOf(FileManager.getString("splitTunnelMode", SplitTunnelMode.DISABLED.toString())); } catch (Exception e) { splitTunnelMode = SplitTunnelMode.DISABLED; } diff --git a/app/src/main/java/org/bepass/oblivion/service/OblivionVpnService.java b/app/src/main/java/org/bepass/oblivion/service/OblivionVpnService.java index 88446a77..e7eef75e 100644 --- a/app/src/main/java/org/bepass/oblivion/service/OblivionVpnService.java +++ b/app/src/main/java/org/bepass/oblivion/service/OblivionVpnService.java @@ -13,6 +13,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.Message; import android.os.Messenger; import android.os.ParcelFileDescriptor; @@ -24,8 +25,6 @@ import androidx.core.app.NotificationChannelCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; -import androidx.core.content.ContextCompat; - import org.bepass.oblivion.enums.ConnectionState; import org.bepass.oblivion.interfaces.ConnectionStateChangeListener; import org.bepass.oblivion.R; @@ -41,6 +40,8 @@ import java.net.ServerSocket; import java.util.HashMap; import java.util.HashSet; +import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; @@ -67,7 +68,7 @@ public class OblivionVpnService extends VpnService { private static final String PRIVATE_VLAN6_CLIENT = "fdfe:dcba:9876::1"; private final Handler handler = new Handler(); private final Messenger serviceMessenger = new Messenger(new IncomingHandler(this)); - private final Map connectionStateObservers = new HashMap<>(); + private static final Map connectionStateObservers = new HashMap<>(); private final Runnable logRunnable = new Runnable() { @Override public void run() { @@ -79,31 +80,32 @@ public void run() { e.printStackTrace(); } } - handler.postDelayed(this, 2000); // Poll every 2 seconds + // Adding jitter to avoid exact timing + long jitter = (long) (Math.random() * 500); // Random delay between 0 to 500ms + handler.postDelayed(this, 2000 + jitter); // Poll every ~2 seconds with some jitter } }; // For JNI Calling in a new threa - private final Executor executorService = Executors.newSingleThreadExecutor(); + private static final Executor executorService = Executors.newSingleThreadExecutor(); // For PingHTTPTestConnection to don't busy-waiting - private ScheduledExecutorService scheduler; + private static ScheduledExecutorService scheduler; private Notification notification; - private ParcelFileDescriptor mInterface; + private static ParcelFileDescriptor mInterface; private String bindAddress; - private FileManager fileManager; private static PowerManager.WakeLock wLock; - private ConnectionState lastKnownState = ConnectionState.DISCONNECTED; - + private static ConnectionState lastKnownState = ConnectionState.DISCONNECTED; public static synchronized void startVpnService(Context context) { - Intent intent = new Intent(context, OblivionVpnService.class); + FileManager.initialize(context); + Intent intent = new Intent(context.getApplicationContext(), OblivionVpnService.class); intent.setAction(OblivionVpnService.FLAG_VPN_START); - ContextCompat.startForegroundService(context, intent); + context.startService(intent); } - public static synchronized void stopVpnService(Context context) { - Intent intent = new Intent(context, OblivionVpnService.class); + Intent intent = new Intent(context.getApplicationContext(), OblivionVpnService.class); intent.setAction(OblivionVpnService.FLAG_VPN_STOP); - ContextCompat.startForegroundService(context, intent); + context.startService(intent); + } public static void registerConnectionStateObserver(String key, Messenger serviceMessenger, ConnectionStateChangeListener observer) { // Create a message for the service @@ -147,33 +149,44 @@ public static Map splitHostAndPort(String hostPort) { String host; int port = -1; // Default port value if not specified - // Check if the host part is an IPv6 address (enclosed in square brackets) - if (hostPort.startsWith("[")) { - int closingBracketIndex = hostPort.indexOf(']'); - if (closingBracketIndex > 0) { - host = hostPort.substring(1, closingBracketIndex); - if (hostPort.length() > closingBracketIndex + 1 && hostPort.charAt(closingBracketIndex + 1) == ':') { - // There's a port number after the closing bracket - port = Integer.parseInt(hostPort.substring(closingBracketIndex + 2)); + try { + // Check if the host part is an IPv6 address (enclosed in square brackets) + if (hostPort.startsWith("[")) { + int closingBracketIndex = hostPort.indexOf(']'); + if (closingBracketIndex > 0) { + host = hostPort.substring(1, closingBracketIndex); + if (hostPort.length() > closingBracketIndex + 1 && hostPort.charAt(closingBracketIndex + 1) == ':') { + // There's a port number after the closing bracket + String portStr = hostPort.substring(closingBracketIndex + 2); + if (!portStr.isEmpty()) { + port = Integer.parseInt(portStr); + } + } + } else { + throw new IllegalArgumentException("Invalid IPv6 address format"); } } else { - throw new IllegalArgumentException("Invalid IPv6 address format"); - } - } else { - // Handle IPv4 or hostname (split at the last colon) - int lastColonIndex = hostPort.lastIndexOf(':'); - if (lastColonIndex > 0) { - host = hostPort.substring(0, lastColonIndex); - port = Integer.parseInt(hostPort.substring(lastColonIndex + 1)); - } else { - host = hostPort; // No port specified + // Handle IPv4 or hostname (split at the last colon) + int lastColonIndex = hostPort.lastIndexOf(':'); + if (lastColonIndex > 0) { + host = hostPort.substring(0, lastColonIndex); + String portStr = hostPort.substring(lastColonIndex + 1); + if (!portStr.isEmpty()) { + port = Integer.parseInt(portStr); + } + } else { + host = hostPort; // No port specified + } } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid port number format in: " + hostPort, e); } result.put(host, port); return result; } + private static int findFreePort() { try (ServerSocket socket = new ServerSocket(0)) { socket.setReuseAddress(true); @@ -221,6 +234,9 @@ public static String isLocalPortInUse(String bindAddress) { return "exception"; } int socksPort = result.values().iterator().next(); + if (socksPort == -1) { + return "false"; // Consider no port specified as not in use + } try { // ServerSocket try to open a LOCAL port new ServerSocket(socksPort).close(); @@ -232,8 +248,9 @@ public static String isLocalPortInUse(String bindAddress) { } } + private Set getSplitTunnelApps() { - return fileManager.getStringSet("splitTunnelApps", new HashSet<>()); + return FileManager.getStringSet("splitTunnelApps", new HashSet<>()); } @@ -279,19 +296,21 @@ private void stopForegroundService() { } private String getBindAddress() { - String port = fileManager.getString("USERSETTING_port"); - boolean enableLan = fileManager.getBoolean("USERSETTING_lan"); - if (OblivionVpnService.isLocalPortInUse("127.0.0.1:" + port).equals("true")) { + String port = FileManager.getString("USERSETTING_port"); + boolean enableLan = FileManager.getBoolean("USERSETTING_lan"); + String bindAddress = "127.0.0.1:" + port; + + if (isLocalPortInUse(bindAddress).equals("true")) { port = String.valueOf(findFreePort()); } - String Bind = ""; - Bind += "127.0.0.1:" + port; + String bind = "127.0.0.1:" + port; if (enableLan) { - Bind = "0.0.0.0:" + port; + bind = "0.0.0.0:" + port; } - return Bind; + return bind; } + @Override public IBinder onBind(Intent intent) { String action = intent != null ? intent.getAction() : null; @@ -314,35 +333,32 @@ private void clearLogFile() { } private void start() { - // If there's an existing running service, revoke it first if (lastKnownState != ConnectionState.DISCONNECTED) { onRevoke(); } - - fileManager = FileManager.getInstance(this); - setLastKnownState(ConnectionState.CONNECTING); Log.i(TAG, "Clearing Logs"); clearLogFile(); Log.i(TAG, "Create Notification"); - createNotification(); - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) { - startForeground(1, notification); - } else { - startForeground(1, notification, FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED); - } if (wLock == null) { wLock = ((PowerManager) getSystemService(Context.POWER_SERVICE)).newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "oblivion:vpn"); wLock.setReferenceCounted(false); - wLock.acquire(3*60*1000L /*3 minutes*/); + wLock.acquire(3 * 60 * 1000L /*3 minutes*/); } executorService.execute(() -> { bindAddress = getBindAddress(); Log.i(TAG, "Configuring VPN service"); try { + createNotification(); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) { + startForeground(1, notification); + } else { + startForeground(1, notification, FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED); + } configure(); + } catch (Exception e) { onRevoke(); e.printStackTrace(); @@ -354,36 +370,27 @@ private void start() { onRevoke(); } setLastKnownState(state); - // Re-create the notification when the connection state changes createNotification(); - startForeground(1, notification); // Start foreground again after connection + startForeground(1, notification); }); }); } @Override - public synchronized int onStartCommand(Intent intent, int flags, int startId) { - if (intent == null) { - return START_NOT_STICKY; - } - - String action = intent.getAction(); - if (action == null) { - return START_NOT_STICKY; - } + public int onStartCommand(Intent intent, int flags, int startId) { + String action = intent != null ? intent.getAction() : null; - switch (action) { - case FLAG_VPN_START: + if (FLAG_VPN_START.equals(action)) { + // Start VPN + if (lastKnownState == ConnectionState.DISCONNECTED) { start(); - return START_STICKY; - - case FLAG_VPN_STOP: - onRevoke(); - return START_NOT_STICKY; - - default: - return START_NOT_STICKY; + } + } else if (FLAG_VPN_STOP.equals(action)) { + // Stop VPN + onRevoke(); } + + return START_STICKY; } @Override @@ -401,73 +408,70 @@ public void onDestroy() { wLock = null; } } - @Override public void onRevoke() { + // Set the last known state to DISCONNECTED setLastKnownState(ConnectionState.DISCONNECTED); - Log.i(TAG, "Stopping VPN"); + // Stop the foreground service stopForegroundService(); + Log.e(TAG, "VPN service is being forcefully stopped"); // Release the wake lock if held - try { - if (wLock != null && wLock.isHeld()) { - wLock.release(); - wLock = null; - } - } catch (Exception e) { - Log.e(TAG, "Error releasing wake lock", e); + if (wLock != null && wLock.isHeld()) { + wLock.release(); + wLock = null; + Log.e(TAG, "Wake lock released"); + } else { + Log.w(TAG, "No wake lock to release"); } // Close the VPN interface try { - if (mInterface != null) { - mInterface.close(); + if (!FileManager.getBoolean("USERSETTING_proxymode")) { + if (mInterface != null) { + mInterface.close(); + mInterface = null; // Set to null to ensure it's not reused + Log.e(TAG, "VPN interface closed successfully"); + } else { + Log.w(TAG, "VPN interface was already null"); + } + } else { + Log.w(TAG, "Proxy mode is enabled; skipping VPN interface closure"); } } catch (IOException e) { - Log.e(TAG, "Error closing the VPN interface", e); + Log.e(TAG, "Critical error closing the VPN interface", e); } // Stop Tun2socks try { Tun2socks.stop(); + Log.e(TAG, "Tun2socks stopped successfully"); } catch (Exception e) { - Log.e(TAG, "Error stopping Tun2socks", e); + Log.e(TAG, "Critical error stopping Tun2socks", e); } - + stopForegroundService(); + stopSelf(); // Shutdown executor service if (executorService instanceof ExecutorService) { ExecutorService service = (ExecutorService) executorService; - service.shutdown(); // Attempt to gracefully shutdown try { - // Wait a certain amount of time for tasks to complete - if (!service.awaitTermination(500, TimeUnit.MILLISECONDS)) { - service.shutdownNow(); // Forcefully terminate if tasks are not completed + service.shutdownNow(); // Attempt to forcibly shutdown + if (!service.awaitTermination(300, TimeUnit.MILLISECONDS)) { + Log.e(TAG, "ExecutorService did not terminate in the specified time"); + List droppedTasks = service.shutdownNow(); + Log.e(TAG, "ExecutorService was forcibly shut down. Dropped tasks: " + droppedTasks); } - } catch (InterruptedException e) { - service.shutdownNow(); // Forcefully terminate if interrupted - Thread.currentThread().interrupt(); // Restore interrupted status - Log.e(TAG, "Executor service termination interrupted", e); - } - } - - // Shutdown scheduler if it is running - shutdownScheduler(); - Log.i(TAG, "VPN stopped successfully"); - } - private void shutdownScheduler() { - if (scheduler != null && !scheduler.isShutdown()) { - scheduler.shutdown(); - try { - if (!scheduler.awaitTermination(500, TimeUnit.MILLISECONDS)) { - scheduler.shutdownNow(); - } - } catch (InterruptedException e) { - scheduler.shutdownNow(); + Log.e(TAG, "ExecutorService shut down successfully"); + } catch (InterruptedException ie) { + Log.e(TAG, "Interrupted during ExecutorService shutdown", ie); Thread.currentThread().interrupt(); - Log.e(TAG, "Scheduler termination interrupted", e); } + } else { + Log.w(TAG, "ExecutorService was not an instance of ExecutorService"); } + + Log.e(TAG, "VPN stopped successfully or encountered errors. Check logs for details."); } private void publishConnectionState(ConnectionState state) { @@ -493,20 +497,27 @@ private void publishConnectionStateTo(String observerKey, ConnectionState state) } private void setLastKnownState(ConnectionState lastKnownState) { - this.lastKnownState = lastKnownState; + OblivionVpnService.lastKnownState = lastKnownState; publishConnectionState(lastKnownState); } private String getNotificationText() { - boolean usePsiphon = fileManager.getBoolean("USERSETTING_psiphon"); - boolean useWarp = fileManager.getBoolean("USERSETTING_gool"); + boolean usePsiphon = FileManager.getBoolean("USERSETTING_psiphon"); + boolean useWarp = FileManager.getBoolean("USERSETTING_gool"); + boolean proxyMode = FileManager.getBoolean("USERSETTING_proxymode"); + String portInUse = FileManager.getString("USERSETTING_port"); + String notificationText; + String proxyText = proxyMode ? String.format(Locale.getDefault(), " on socks5 proxy at 127.0.0.1:%s", portInUse) : ""; if (usePsiphon) { - return "Psiphon in Warp"; + notificationText = "Psiphon in Warp" + proxyText; } else if (useWarp) { - return "Warp in Warp"; + notificationText = "Warp in Warp" + proxyText; + } else { + notificationText = "Warp" + proxyText; } - return "Warp"; + + return notificationText; } private void createNotification() { @@ -546,98 +557,125 @@ public void removeConnectionStateObserver(String key) { } private void configure() throws Exception { - VpnService.Builder builder = new VpnService.Builder(); + boolean proxyModeEnabled = FileManager.getBoolean("USERSETTING_proxymode"); + + if (proxyModeEnabled) { + // Syncing FileManager + + // Proxy mode logic + StartOptions so = new StartOptions(); + so.setPath(getApplicationContext().getFilesDir().getAbsolutePath()); + so.setVerbose(true); + so.setEndpoint(getEndpoint()); + so.setBindAddress(bindAddress); + so.setLicense(FileManager.getString("USERSETTING_license", "").trim()); + so.setDNS("1.1.1.1"); + so.setEndpointType(FileManager.getInt("USERSETTING_endpoint_type")); + + if (FileManager.getBoolean("USERSETTING_psiphon", false)) { + so.setPsiphonEnabled(true); + so.setCountry(FileManager.getString("USERSETTING_country", "AT").trim()); + } else if (FileManager.getBoolean("USERSETTING_gool", false)) { + so.setGool(true); + } + + // Start tun2socks in proxy mode + Tun2socks.start(so); + + } else { + // VPN mode logic + VpnService.Builder builder = new VpnService.Builder(); + configureVpnBuilder(builder); + + mInterface = builder.establish(); + if (mInterface == null) throw new RuntimeException("failed to establish VPN interface"); + Log.i(TAG, "Interface created"); + + StartOptions so = new StartOptions(); + so.setPath(getApplicationContext().getFilesDir().getAbsolutePath()); + so.setVerbose(true); + so.setEndpoint(getEndpoint()); + so.setBindAddress(bindAddress); + so.setLicense(FileManager.getString("USERSETTING_license", "").trim()); + so.setDNS("1.1.1.1"); + so.setEndpointType(FileManager.getInt("USERSETTING_endpoint_type")); + so.setTunFd(mInterface.getFd()); + + if (FileManager.getBoolean("USERSETTING_psiphon", false)) { + so.setPsiphonEnabled(true); + so.setCountry(FileManager.getString("USERSETTING_country", "AT").trim()); + } else if (FileManager.getBoolean("USERSETTING_gool", false)) { + so.setGool(true); + } + + // Start tun2socks with VPN + Tun2socks.start(so); + } + } + private void configureVpnBuilder(VpnService.Builder builder) throws Exception { builder.setSession("oblivion") - .setMtu(1500) - .addAddress(PRIVATE_VLAN4_CLIENT, 30) - .addAddress(PRIVATE_VLAN6_CLIENT, 126) - .addDnsServer("1.1.1.1") - .addDnsServer("1.0.0.1") - .addDisallowedApplication(getPackageName()) - .addRoute("0.0.0.0", 0) - .addRoute("::", 0); - - fileManager.getStringSet("splitTunnelApps", new HashSet<>()); - SplitTunnelMode splitTunnelMode = SplitTunnelMode.getSplitTunnelMode(fileManager); + .setMtu(1500) + .addAddress(PRIVATE_VLAN4_CLIENT, 30) + .addAddress(PRIVATE_VLAN6_CLIENT, 126) + .addDnsServer("1.1.1.1") + .addDnsServer("1.0.0.1") + .addDisallowedApplication(getPackageName()) + .addRoute("0.0.0.0", 0) + .addRoute("::", 0); + + // Determine split tunnel mode + SplitTunnelMode splitTunnelMode = SplitTunnelMode.getSplitTunnelMode(); if (splitTunnelMode == SplitTunnelMode.BLACKLIST) { - for (String packageName : getSplitTunnelApps()) { + Set splitTunnelApps = getSplitTunnelApps(); + for (String packageName : splitTunnelApps) { try { builder.addDisallowedApplication(packageName); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); + } catch (PackageManager.NameNotFoundException ignored) { } } } + } - mInterface = builder.establish(); - if (mInterface == null) throw new RuntimeException("failed to establish VPN interface"); - Log.i(TAG, "Interface created"); - - String endpoint = fileManager.getString("USERSETTING_endpoint", "engage.cloudflareclient.com:2408").trim(); - if (endpoint.equals("engage.cloudflareclient.com:2408")) { - endpoint = ""; - } - int endpointType = fileManager.getInt("USERSETTING_endpoint_type"); - - String license = fileManager.getString("USERSETTING_license", "").trim(); - boolean enablePsiphon = fileManager.getBoolean("USERSETTING_psiphon", false); - String country = fileManager.getString("USERSETTING_country", "AT").trim(); - boolean enableGool = fileManager.getBoolean("USERSETTING_gool", false); - - StartOptions so = new StartOptions(); - so.setPath(getApplicationContext().getFilesDir().getAbsolutePath()); - so.setVerbose(true); - so.setEndpoint(endpoint); - so.setBindAddress(bindAddress); - so.setLicense(license); - so.setDNS("1.1.1.1"); - so.setEndpointType(endpointType); - - if (enablePsiphon && !enableGool) { - so.setPsiphonEnabled(true); - so.setCountry(country); - } - - if (!enablePsiphon && enableGool) { - so.setGool(true); - } - - so.setTunFd(mInterface.getFd()); - - Tun2socks.start(so); + private String getEndpoint() { + String endpoint = FileManager.getString("USERSETTING_endpoint", "engage.cloudflareclient.com:2408").trim(); + return endpoint.equals("engage.cloudflareclient.com:2408") ? "" : endpoint; } private static class IncomingHandler extends Handler { private final WeakReference serviceRef; IncomingHandler(OblivionVpnService service) { + super(Looper.getMainLooper()); // Ensure the handler runs on the main thread serviceRef = new WeakReference<>(service); } @Override public void handleMessage(@NonNull Message msg) { - final Message message = new Message(); - message.copyFrom(msg); OblivionVpnService service = serviceRef.get(); if (service == null) return; + switch (msg.what) { case MSG_CONNECTION_STATE_SUBSCRIBE: { - String key = message.getData().getString("key"); - if (key == null) - throw new RuntimeException("No key was provided for the connection state observer"); - if (service.connectionStateObservers.containsKey(key)) { - //Already subscribed + String key = msg.getData().getString("key"); + if (key == null) { + Log.e("IncomingHandler", "No key was provided for the connection state observer"); return; } - service.addConnectionStateObserver(key, message.replyTo); - service.publishConnectionStateTo(key, service.lastKnownState); + if (connectionStateObservers.containsKey(key)) { + // Already subscribed + return; + } + service.addConnectionStateObserver(key, msg.replyTo); + service.publishConnectionStateTo(key, lastKnownState); break; } case MSG_CONNECTION_STATE_UNSUBSCRIBE: { - String key = message.getData().getString("key"); - if (key == null) - throw new RuntimeException("No observer was specified to unregister"); + String key = msg.getData().getString("key"); + if (key == null) { + Log.e("IncomingHandler", "No observer was specified to unregister"); + return; + } service.removeConnectionStateObserver(key); break; } diff --git a/app/src/main/java/org/bepass/oblivion/ui/InfoActivity.java b/app/src/main/java/org/bepass/oblivion/ui/InfoActivity.java index 9dc6a050..79ef42df 100644 --- a/app/src/main/java/org/bepass/oblivion/ui/InfoActivity.java +++ b/app/src/main/java/org/bepass/oblivion/ui/InfoActivity.java @@ -1,39 +1,39 @@ -package org.bepass.oblivion.ui; + package org.bepass.oblivion.ui; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.widget.ImageView; -import android.widget.RelativeLayout; + import android.content.Intent; + import android.net.Uri; + import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; + import org.bepass.oblivion.R; + import org.bepass.oblivion.base.BaseActivity; + import org.bepass.oblivion.databinding.ActivityInfoBinding; + import org.bepass.oblivion.utils.ThemeHelper; -import org.bepass.oblivion.R; -import org.bepass.oblivion.base.BaseActivity; -import org.bepass.oblivion.databinding.ActivityInfoBinding; + public class InfoActivity extends BaseActivity { -public class InfoActivity extends BaseActivity { + @Override + protected int getLayoutResourceId() { + return R.layout.activity_info; + } - @Override - protected int getLayoutResourceId() { - return R.layout.activity_info; - } + @Override + protected int getStatusBarColor() { + return R.color.status_bar_color; + } - @Override - protected int getStatusBarColor() { - return R.color.status_bar_color; - } + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + // Update background based on current theme + ThemeHelper.getInstance().updateActivityBackground(binding.getRoot()); - binding.githubLayout.setOnClickListener(v -> { - Uri uri = Uri.parse("https://github.com/bepass-org/oblivion"); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - startActivity(intent); - }); + binding.githubLayout.setOnClickListener(v -> { + Uri uri = Uri.parse("https://github.com/bepass-org/oblivion"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + }); - binding.back.setOnClickListener(v -> getOnBackPressedDispatcher().onBackPressed()); + binding.back.setOnClickListener(v -> getOnBackPressedDispatcher().onBackPressed()); + } } -} diff --git a/app/src/main/java/org/bepass/oblivion/ui/LogActivity.java b/app/src/main/java/org/bepass/oblivion/ui/LogActivity.java index 26e74404..5d9d2194 100644 --- a/app/src/main/java/org/bepass/oblivion/ui/LogActivity.java +++ b/app/src/main/java/org/bepass/oblivion/ui/LogActivity.java @@ -6,18 +6,19 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.widget.Button; -import android.widget.ImageView; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ProgressBar; import android.widget.ScrollView; import android.widget.TextView; - -import androidx.appcompat.app.AppCompatActivity; - -import com.google.android.material.button.MaterialButton; +import android.widget.Toast; import org.bepass.oblivion.R; import org.bepass.oblivion.base.BaseActivity; import org.bepass.oblivion.databinding.ActivityLogBinding; +import org.bepass.oblivion.utils.ISPUtils; +import org.bepass.oblivion.utils.ThemeHelper; import java.io.BufferedReader; import java.io.IOException; @@ -31,6 +32,7 @@ public class LogActivity extends BaseActivity { private final Handler handler = new Handler(Looper.getMainLooper()); private boolean isUserScrollingUp = false; private Runnable logUpdater; + private FrameLayout progressBar; @Override protected int getLayoutResourceId() { @@ -45,7 +47,11 @@ protected int getStatusBarColor() { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Update background based on current theme + ThemeHelper.getInstance().updateActivityBackground(binding.getRoot()); + // Initialize the ProgressBar + progressBar = findViewById(R.id.progress_container); binding.back.setOnClickListener(v -> getOnBackPressedDispatcher().onBackPressed()); binding.copytoclip.setOnClickListener(v -> copyLast100LinesToClipboard()); setupScrollListener(); @@ -101,22 +107,61 @@ private void readLogsFromFile() { } private void copyLast100LinesToClipboard() { - String logText = binding.logs.getText().toString(); - String[] logLines = logText.split("\n"); - int totalLines = logLines.length; + // Show progress bar + progressBar.setVisibility(View.VISIBLE); - // Use Deque to efficiently get the last 100 lines - Deque last100Lines = new ArrayDeque<>(100); - last100Lines.addAll(Arrays.asList(logLines).subList(Math.max(0, totalLines - 100), totalLines)); + ISPUtils.fetchISPInfo(new ISPUtils.ISPCallback() { + @Override + public void onISPInfoReceived(String isp) { + runOnUiThread(() -> { + // Hide progress bar + progressBar.setVisibility(View.GONE); + + String logText = binding.logs.getText().toString(); + String[] logLines = logText.split("\n"); + int totalLines = logLines.length; + + // Use Deque to efficiently get the last 100 lines + Deque last100Lines = new ArrayDeque<>(100); + last100Lines.addAll(Arrays.asList(logLines).subList(Math.max(0, totalLines - 100), totalLines)); + + StringBuilder sb = new StringBuilder(); + for (String line : last100Lines) { + sb.append(line).append("\n"); + } + + // Add ISP information + sb.append("\n=====\nISP: ").append(isp).append("\n"); + + String last100Log = sb.toString(); + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Log", last100Log); + clipboard.setPrimaryClip(clip); + + showCopiedToClipboardToast(); + }); + } - StringBuilder sb = new StringBuilder(); - for (String line : last100Lines) { - sb.append(line).append("\n"); - } + @Override + public void onError(Exception e) { + runOnUiThread(() -> { + // Hide progress bar + progressBar.setVisibility(View.GONE); + + e.printStackTrace(); + Toast.makeText(LogActivity.this, "Error fetching ISP information.", Toast.LENGTH_SHORT).show(); + }); + } + }); + } + + private void showCopiedToClipboardToast() { + LayoutInflater inflater = getLayoutInflater(); + View layout = inflater.inflate(R.layout.toast, findViewById(R.id.toast_layout)); - String last100Log = sb.toString(); - ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("Log", last100Log); - clipboard.setPrimaryClip(clip); + Toast toast = new Toast(getApplicationContext()); + toast.setDuration(Toast.LENGTH_SHORT); + toast.setView(layout); + toast.show(); } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/bepass/oblivion/ui/MainActivity.java b/app/src/main/java/org/bepass/oblivion/ui/MainActivity.java index 38c65d7b..83e94dfe 100644 --- a/app/src/main/java/org/bepass/oblivion/ui/MainActivity.java +++ b/app/src/main/java/org/bepass/oblivion/ui/MainActivity.java @@ -6,14 +6,9 @@ import android.Manifest; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.ConnectivityManager; -import android.net.Network; -import android.net.NetworkCapabilities; -import android.net.NetworkInfo; import android.os.Build; import android.os.Bundle; -import android.os.Handler; +import android.util.Log; import android.view.View; import android.widget.Toast; @@ -30,15 +25,16 @@ import org.bepass.oblivion.R; import org.bepass.oblivion.base.StateAwareBaseActivity; import org.bepass.oblivion.databinding.ActivityMainBinding; +import org.bepass.oblivion.utils.ThemeHelper; +import org.bepass.oblivion.utils.NetworkUtils; -import java.util.HashSet; -import java.util.Set; +import java.util.Locale; public class MainActivity extends StateAwareBaseActivity { private long backPressedTime; private Toast backToast; private LocaleHandler localeHandler; - private final Handler handler = new Handler(); + private ActivityResultLauncher vpnPermissionLauncher; public static void start(Context context) { Intent starter = new Intent(context, MainActivity.class); @@ -58,51 +54,52 @@ protected int getStatusBarColor() { @Override protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - cleanOrMigrateSettings(); - setupUI(); // Initialize the LocaleHandler and set the locale localeHandler = new LocaleHandler(this); + FileManager.initialize(this); + super.onCreate(savedInstanceState); + // Update background based on current theme + ThemeHelper.getInstance().updateActivityBackground(binding.getRoot()); + FileManager.cleanOrMigrateSettings(binding.getRoot().getContext()); + setupUI(); setupVPNConnection(); - // Request permission to create push notifications requestNotificationPermission(); - - // Set the behaviour of the back button handleBackPress(); } private void setupVPNConnection() { - ActivityResultLauncher vpnPermissionLauncher = registerForActivityResult( + vpnPermissionLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { - if (result.getResultCode() != RESULT_OK) { - Toast.makeText(this, "Really!?", Toast.LENGTH_LONG).show(); + if (result.getResultCode() == RESULT_OK) { + handleVpnSwitch(binding.switchButton.isChecked()); + } else { + Toast.makeText(this, "Permission required to start VPN", Toast.LENGTH_LONG).show(); + binding.switchButton.setChecked(false); } - binding.switchButton.setChecked(false); }); - binding.switchButton.setOnCheckedChangeListener((view, isChecked) -> { - if (!isChecked) { - if (!lastKnownConnectionState.isDisconnected()) { - stopVpnService(this); + binding.switchButton.setOnCheckedChangeListener((view, isChecked) -> handleVpnSwitch(isChecked)); + } + + private void handleVpnSwitch(boolean enableVpn) { + if (enableVpn) { + if (lastKnownConnectionState.isDisconnected()) { + Intent vpnIntent = OblivionVpnService.prepare(this); + if (vpnIntent != null) { + vpnPermissionLauncher.launch(vpnIntent); + } else { + startVpnService(binding.getRoot().getContext()); } - return; - } - Intent vpnIntent = OblivionVpnService.prepare(this); - if (vpnIntent != null) { - vpnPermissionLauncher.launch(vpnIntent); - return; + NetworkUtils.monitorInternetConnection(lastKnownConnectionState, this); + } else if (lastKnownConnectionState.isConnecting()) { + stopVpnService(binding.getRoot().getContext()); } - if (lastKnownConnectionState.isConnecting()) { + } else { + if (!lastKnownConnectionState.isDisconnected()) { stopVpnService(this); - return; } - if (lastKnownConnectionState.isDisconnected()) { - startVpnService(this); - } - monitorInternetConnection(); - }); + } } private void handleBackPress() { @@ -113,8 +110,7 @@ public void handleOnBackPressed() { if (backToast != null) backToast.cancel(); finish(); } else { - if (backToast != null) - backToast.cancel(); + if (backToast != null) backToast.cancel(); backToast = Toast.makeText(MainActivity.this, "برای خروج، دوباره بازگشت را فشار دهید.", Toast.LENGTH_SHORT); backToast.show(); } @@ -143,93 +139,34 @@ private void setupUI() { binding.switchButtonFrame.setOnClickListener(v -> binding.switchButton.toggle()); } - private void monitorInternetConnection() { - handler.postDelayed(new Runnable() { - @Override - public void run() { - if (!lastKnownConnectionState.isDisconnected()) { - checkInternetConnectionAndDisconnectVPN(); - handler.postDelayed(this, 3000); // Check every 3 seconds - } - } - }, 5000); // Start checking after 5 seconds - } - - // Check internet connectivity - private boolean isConnectedToInternet() { - ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivityManager != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - Network activeNetwork = connectivityManager.getActiveNetwork(); - if (activeNetwork != null) { - NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork); - return networkCapabilities != null && - (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || - networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)); - } - } else { - // For API levels below 23, use the deprecated method - NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo(); - return activeNetwork != null && activeNetwork.isConnectedOrConnecting(); - } - } - return false; - } - - // Periodically check internet connection and disconnect VPN if not connected - private void checkInternetConnectionAndDisconnectVPN() { - if (!isConnectedToInternet()) { - stopVpnService(this); - } - } - - protected void cleanOrMigrateSettings() { - // Get the global FileManager instance - FileManager fileManager = FileManager.getInstance(getApplicationContext()); - - if (!fileManager.getBoolean("isFirstValueInit")) { - fileManager.set("USERSETTING_endpoint", "engage.cloudflareclient.com:2408"); - fileManager.set("USERSETTING_port", "8086"); - fileManager.set("USERSETTING_gool", false); - fileManager.set("USERSETTING_psiphon", false); - fileManager.set("USERSETTING_lan", false); - fileManager.set("isFirstValueInit", true); - } - - // Check which split mode apps have been uninstalled and remove them from the list in settings - Set splitApps = fileManager.getStringSet("splitTunnelApps", new HashSet<>()); - Set shouldKeep = new HashSet<>(); - final PackageManager pm = getApplicationContext().getPackageManager(); - for (String packageName : splitApps) { - try { - pm.getPackageInfo(packageName, PackageManager.GET_META_DATA); - } catch (PackageManager.NameNotFoundException ignored) { - continue; - } - shouldKeep.add(packageName); - } - fileManager.set("splitTunnelApps", shouldKeep); - } - @NonNull @Override public String getKey() { return "mainActivity"; } + @Override + protected void onResume() { + super.onResume(); + observeConnectionStatus(); + } + @Override public void onConnectionStateChange(ConnectionState state) { - switch (state) { - case DISCONNECTED: - updateUIForDisconnectedState(); - break; - case CONNECTING: - updateUIForConnectingState(); - break; - case CONNECTED: - updateUIForConnectedState(); - break; - } + runOnUiThread(() -> { + Log.d("MainActivity", "Connection state changed to: " + state); + switch (state) { + case DISCONNECTED: + updateUIForDisconnectedState(); + break; + case CONNECTING: + updateUIForConnectingState(); + break; + case CONNECTED: + updateUIForConnectedState(); + break; + } + }); } private void updateUIForDisconnectedState() { @@ -250,7 +187,11 @@ private void updateUIForConnectingState() { private void updateUIForConnectedState() { binding.switchButton.setEnabled(true); - binding.stateText.setText(R.string.connected); + if (FileManager.getBoolean("USERSETTING_proxymode")) { + binding.stateText.setText(String.format(Locale.getDefault(), "socks5 %s on 127.0.0.1:%s", getString(R.string.connected), FileManager.getString("USERSETTING_port"))); + } else { + binding.stateText.setText(R.string.connected); + } binding.switchButton.setChecked(true, false); binding.ipProgressBar.setVisibility(View.GONE); PublicIPUtils.getInstance().getIPDetails((details) -> { @@ -261,4 +202,4 @@ private void updateUIForConnectedState() { } }); } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/bepass/oblivion/ui/SettingsActivity.java b/app/src/main/java/org/bepass/oblivion/ui/SettingsActivity.java index 96f6ea56..6361ba64 100644 --- a/app/src/main/java/org/bepass/oblivion/ui/SettingsActivity.java +++ b/app/src/main/java/org/bepass/oblivion/ui/SettingsActivity.java @@ -5,21 +5,21 @@ import android.content.Intent; import android.os.Bundle; -import android.util.Pair; +import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.CheckBox; import android.widget.CompoundButton; -import android.widget.Spinner; import androidx.activity.OnBackPressedCallback; +import androidx.appcompat.app.AppCompatDelegate; +import org.bepass.oblivion.EndpointsBottomSheet; import org.bepass.oblivion.enums.ConnectionState; import org.bepass.oblivion.utils.CountryUtils; import org.bepass.oblivion.EditSheet; import org.bepass.oblivion.utils.FileManager; -import org.bepass.oblivion.utils.LocaleHelper; import org.bepass.oblivion.service.OblivionVpnService; import org.bepass.oblivion.R; import org.bepass.oblivion.interfaces.SheetsCallBack; @@ -28,10 +28,15 @@ import org.bepass.oblivion.databinding.ActivitySettingsBinding; import org.bepass.oblivion.utils.ThemeHelper; +import java.io.File; +import java.util.Objects; + +import kotlin.Triple; + public class SettingsActivity extends StateAwareBaseActivity { - private FileManager fileManager; private CheckBox.OnCheckedChangeListener psiphonListener; private CheckBox.OnCheckedChangeListener goolListener; + private CompoundButton.OnCheckedChangeListener proxyModeListener; private void setCheckBoxWithoutTriggeringListener(CheckBox checkBox, boolean isChecked, CheckBox.OnCheckedChangeListener listener) { checkBox.setOnCheckedChangeListener(null); // Temporarily detach the listener @@ -52,7 +57,10 @@ protected int getStatusBarColor() { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - fileManager = FileManager.getInstance(this); + // Update background based on current theme + ThemeHelper.getInstance().updateActivityBackground(binding.getRoot()); + // Set Current Values + settingBasicValuesFromSPF(); if (isBatteryOptimizationEnabled(this)) { binding.batteryOptimizationLayout.setOnClickListener(view -> { @@ -63,7 +71,6 @@ protected void onCreate(Bundle savedInstanceState) { binding.batteryOptLine.setVisibility(View.GONE); } - binding.back.setOnClickListener(v -> getOnBackPressedDispatcher().onBackPressed()); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @@ -81,8 +88,16 @@ public void handleOnBackPressed() { }); SheetsCallBack sheetsCallBack = this::settingBasicValuesFromSPF; - // Listen to Changes - binding.endpointLayout.setOnClickListener(v -> (new EditSheet(this, getString(R.string.endpointText), "endpoint", sheetsCallBack)).start()); + + binding.endpointLayout.setOnClickListener(v -> { + EndpointsBottomSheet bottomSheet = new EndpointsBottomSheet(); + bottomSheet.setEndpointSelectionListener(content -> { + FileManager.set("USERSETTING_endpoint", content); + binding.endpoint.setText(content); + }); + bottomSheet.show(getSupportFragmentManager(), bottomSheet.getTag()); + }); + binding.portLayout.setOnClickListener(v -> (new EditSheet(this, getString(R.string.portTunText), "port", sheetsCallBack)).start()); binding.licenseLayout.setOnClickListener(v -> (new EditSheet(this, getString(R.string.licenseText), "license", sheetsCallBack)).start()); @@ -92,101 +107,133 @@ public void handleOnBackPressed() { binding.country.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { + public void onItemSelected(AdapterView parent, View view, int position, long id) { String name = parent.getItemAtPosition(position).toString(); - Pair codeAndName = CountryUtils.getCountryCode(ApplicationLoader.getAppCtx(), name); - fileManager.set("USERSETTING_country", codeAndName.first); + Triple codeAndName = CountryUtils.getCountryCode(ApplicationLoader.getAppCtx(), name); + FileManager.set("USERSETTING_country", codeAndName.getFirst()); + FileManager.set("USERSETTING_country_index", position); // Save the selected country index } @Override - public void onNothingSelected(AdapterView parent) { + public void onNothingSelected(AdapterView parent) { } }); binding.splitTunnelLayout.setOnClickListener(v -> startActivity(new Intent(this, SplitTunnelActivity.class))); - // Set Current Values - settingBasicValuesFromSPF(); - binding.goolLayout.setOnClickListener(v -> binding.gool.setChecked(!binding.gool.isChecked())); binding.lanLayout.setOnClickListener(v -> binding.lan.setChecked(!binding.lan.isChecked())); binding.psiphonLayout.setOnClickListener(v -> binding.psiphon.setChecked(!binding.psiphon.isChecked())); - binding.lan.setOnCheckedChangeListener((buttonView, isChecked) -> fileManager.set("USERSETTING_lan", isChecked)); - // Initialize the listeners + binding.lan.setOnCheckedChangeListener((buttonView, isChecked) -> FileManager.set("USERSETTING_lan", isChecked)); + psiphonListener = (buttonView, isChecked) -> { - fileManager.set("USERSETTING_psiphon", isChecked); + FileManager.set("USERSETTING_psiphon", isChecked); if (isChecked && binding.gool.isChecked()) { setCheckBoxWithoutTriggeringListener(binding.gool, false, goolListener); - fileManager.set("USERSETTING_gool", false); + FileManager.set("USERSETTING_gool", false); } binding.countryLayout.setAlpha(isChecked ? 1f : 0.2f); binding.country.setEnabled(isChecked); }; goolListener = (buttonView, isChecked) -> { - fileManager.set("USERSETTING_gool", isChecked); + FileManager.set("USERSETTING_gool", isChecked); if (isChecked && binding.psiphon.isChecked()) { setCheckBoxWithoutTriggeringListener(binding.psiphon, false, psiphonListener); - fileManager.set("USERSETTING_psiphon", false); + FileManager.set("USERSETTING_psiphon", false); binding.countryLayout.setAlpha(0.2f); binding.country.setEnabled(false); } }; - binding.txtDarkMode.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - binding.checkBoxDarkMode.setChecked(!binding.checkBoxDarkMode.isChecked()); - } - }); + + proxyModeListener = (buttonView, isChecked) -> FileManager.set("USERSETTING_proxymode", isChecked); + + binding.txtDarkMode.setOnClickListener(view -> binding.checkBoxDarkMode.setChecked(!binding.checkBoxDarkMode.isChecked())); + + // Set the initial state of the checkbox based on the current theme binding.checkBoxDarkMode.setChecked(ThemeHelper.getInstance().getCurrentTheme() == ThemeHelper.Theme.DARK); - binding.checkBoxDarkMode.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - ThemeHelper.getInstance().select(ThemeHelper.Theme.DARK); - } else { - ThemeHelper.getInstance().select(ThemeHelper.Theme.LIGHT); - } - } + // Set up the listener to change the theme when the checkbox is toggled + binding.checkBoxDarkMode.setOnCheckedChangeListener((buttonView, isChecked) -> { + // Determine the new theme based on the checkbox state + ThemeHelper.Theme newTheme = isChecked ? ThemeHelper.Theme.DARK : ThemeHelper.Theme.LIGHT; + + // Use ThemeHelper to apply the new theme + ThemeHelper.getInstance().select(newTheme); }); - // Set the listeners to the checkboxes + binding.psiphon.setOnCheckedChangeListener(psiphonListener); binding.gool.setOnCheckedChangeListener(goolListener); + binding.resetAppLayout.setOnClickListener(v -> resetAppData()); + binding.proxyModeLayout.setOnClickListener(v -> binding.proxyMode.performClick()); + binding.proxyMode.setOnCheckedChangeListener(proxyModeListener); } - private int getIndexFromName(Spinner spinner, String name) { - String ccn = CountryUtils.getCountryName(name); - String newname = LocaleHelper.restoreText(this, ccn); - for (int i = 0; i < spinner.getCount(); i++) { - if (spinner.getItemAtPosition(i).toString().equalsIgnoreCase(newname)) { - return i; - } + private void resetAppData() { + clearSharedPreferences(); + + try { + deleteDir(getCacheDir()); + } catch (Exception e) { + e.printStackTrace(); + } + + try { + deleteDir(getFilesDir()); + } catch (Exception e) { + e.printStackTrace(); } - return 0; + Intent intent = new Intent(this, MainActivity.class); + finish(); + startActivity(intent); } - private void settingBasicValuesFromSPF() { - binding.endpoint.setText(fileManager.getString("USERSETTING_endpoint")); - binding.port.setText(fileManager.getString("USERSETTING_port")); - binding.license.setText(fileManager.getString("USERSETTING_license")); + private void clearSharedPreferences() { + FileManager.resetToDefault(); + } - String countryCode = fileManager.getString("USERSETTING_country"); - int index = 0; - if (!countryCode.isEmpty()) { - LocaleHelper.goEn(this); - String countryName = CountryUtils.getCountryName(countryCode); - index = getIndexFromName(binding.country, countryName); - LocaleHelper.restoreLocale(this); + private boolean deleteDir(File dir) { + if (dir != null && dir.isDirectory()) { + String[] children = dir.list(); + for (int i = 0; i < Objects.requireNonNull(children).length; i++) { + boolean success = deleteDir(new File(dir, children[i])); + if (!success) { + return false; + } + } + return dir.delete(); + } else if (dir != null && dir.isFile()) { + return dir.delete(); + } else { + return false; } - binding.country.setSelection(index); + } - binding.psiphon.setChecked(fileManager.getBoolean("USERSETTING_psiphon")); - binding.lan.setChecked(fileManager.getBoolean("USERSETTING_lan")); - binding.gool.setChecked(fileManager.getBoolean("USERSETTING_gool")); + private void settingBasicValuesFromSPF() { + binding.endpoint.setText(FileManager.getString("USERSETTING_endpoint")); + binding.port.setText(FileManager.getString("USERSETTING_port")); + binding.license.setText(FileManager.getString("USERSETTING_license")); + String countryCode = FileManager.getString("USERSETTING_country"); + if (!countryCode.isEmpty()) { + int index = FileManager.getInt("USERSETTING_country_index"); + if (index != 0) { + ArrayAdapter adapter = ArrayAdapter.createFromResource(this, R.array.countries, R.layout.country_item_layout); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + binding.country.post(new Runnable() { + public void run() { + binding.country.setAdapter(adapter); + binding.country.setSelection(index); + } + }); + } + } + binding.psiphon.setChecked(FileManager.getBoolean("USERSETTING_psiphon")); + binding.lan.setChecked(FileManager.getBoolean("USERSETTING_lan")); + binding.gool.setChecked(FileManager.getBoolean("USERSETTING_gool")); + binding.proxyMode.setChecked(FileManager.getBoolean("USERSETTING_proxymode")); if (!binding.psiphon.isChecked()) { binding.countryLayout.setAlpha(0.2f); binding.country.setEnabled(false); diff --git a/app/src/main/java/org/bepass/oblivion/ui/SplashScreenActivity.java b/app/src/main/java/org/bepass/oblivion/ui/SplashScreenActivity.java index 1c9b361a..5214410e 100644 --- a/app/src/main/java/org/bepass/oblivion/ui/SplashScreenActivity.java +++ b/app/src/main/java/org/bepass/oblivion/ui/SplashScreenActivity.java @@ -9,6 +9,7 @@ import org.bepass.oblivion.base.BaseActivity; import org.bepass.oblivion.databinding.ActivitySplashScreenBinding; import org.bepass.oblivion.utils.LocaleHandler; +import org.bepass.oblivion.utils.ThemeHelper; /** * A simple splash screen activity that shows a splash screen for a short duration before navigating @@ -16,7 +17,6 @@ */ @SuppressLint("CustomSplashScreen") public class SplashScreenActivity extends BaseActivity { - /** * Returns the layout resource ID for the splash screen activity. * @@ -44,6 +44,9 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); LocaleHandler localeHandler = new LocaleHandler(this); localeHandler.setPersianAsDefaultLocaleIfNeeds(); + // Update background based on current theme + ThemeHelper.getInstance().updateActivityBackground(binding.getRoot()); + binding.setHandler(new ClickHandler()); // 1 second int SHORT_SPLASH_DISPLAY_LENGTH = 1000; diff --git a/app/src/main/java/org/bepass/oblivion/ui/SplitTunnelActivity.java b/app/src/main/java/org/bepass/oblivion/ui/SplitTunnelActivity.java index 3a73d510..f6d2706f 100644 --- a/app/src/main/java/org/bepass/oblivion/ui/SplitTunnelActivity.java +++ b/app/src/main/java/org/bepass/oblivion/ui/SplitTunnelActivity.java @@ -14,6 +14,7 @@ import org.bepass.oblivion.SplitTunnelOptionsAdapter; import org.bepass.oblivion.base.StateAwareBaseActivity; import org.bepass.oblivion.databinding.ActivitySplitTunnelBinding; +import org.bepass.oblivion.utils.ThemeHelper; public class SplitTunnelActivity extends StateAwareBaseActivity { @@ -31,6 +32,9 @@ protected int getStatusBarColor() { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Update background based on current theme + ThemeHelper.getInstance().updateActivityBackground(binding.getRoot()); + // Handles the back button behaviour binding.back.setOnClickListener(v -> getOnBackPressedDispatcher().onBackPressed()); @@ -51,7 +55,7 @@ protected void onCreate(Bundle savedInstanceState) { @Override public void splitTunnelMode(SplitTunnelMode mode) { StateAwareBaseActivity.setRequireRestartVpnService(true); - FileManager.getInstance(SplitTunnelActivity.this).set("splitTunnelMode", mode.toString()); + FileManager.set("splitTunnelMode", mode.toString()); } @Override diff --git a/app/src/main/java/org/bepass/oblivion/utils/BatteryOptimization.kt b/app/src/main/java/org/bepass/oblivion/utils/BatteryOptimization.kt index b6df2a69..f9c0732d 100644 --- a/app/src/main/java/org/bepass/oblivion/utils/BatteryOptimization.kt +++ b/app/src/main/java/org/bepass/oblivion/utils/BatteryOptimization.kt @@ -1,6 +1,7 @@ package org.bepass.oblivion.utils import android.annotation.SuppressLint +import android.app.Activity import android.app.AlertDialog import android.content.Context import android.content.Intent @@ -9,21 +10,20 @@ import android.os.Build import android.os.PowerManager import android.provider.Settings import android.view.LayoutInflater -import android.widget.Button -import android.widget.TextView +import androidx.databinding.DataBindingUtil import org.bepass.oblivion.R +import org.bepass.oblivion.databinding.DialogBatteryOptimizationBinding /** * Checks if the app is running in restricted background mode. * Returns true if running in restricted mode, false otherwise. */ fun isBatteryOptimizationEnabled(context: Context): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val powerManager = context.getSystemService(Context.POWER_SERVICE) as? PowerManager - powerManager?.isIgnoringBatteryOptimizations(context.packageName) == false - } else { - false + return powerManager?.isIgnoringBatteryOptimizations(context.packageName) == false } + return false } /** @@ -31,13 +31,21 @@ fun isBatteryOptimizationEnabled(context: Context): Boolean { */ @SuppressLint("BatteryLife") fun requestIgnoreBatteryOptimizations(context: Context) { - val intent = Intent().apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - data = Uri.parse("package:${context.packageName}") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager + if (!pm.isIgnoringBatteryOptimizations(context.packageName)) { + val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { + data = Uri.parse("package:${context.packageName}") + } + // Check if context is an Activity + if (context is Activity) { + context.startActivityForResult(intent, 0) // Consider using a valid request code + } else { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } } } - context.startActivity(intent) } @@ -45,21 +53,30 @@ fun requestIgnoreBatteryOptimizations(context: Context) { * Shows a dialog explaining the need for disabling battery optimization and navigates to the app's settings. */ fun showBatteryOptimizationDialog(context: Context) { - val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_battery_optimization, null) + // Inflate the dialog layout using Data Binding + val binding: DialogBatteryOptimizationBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.dialog_battery_optimization, + null, + false + ) - val dialog = AlertDialog.Builder(context).apply { - setView(dialogView) - }.create() + val dialog = AlertDialog.Builder(context) + .setView(binding.root) + .create() - dialogView.findViewById(R.id.dialog_title).text = context.getString(R.string.batteryOpL) - dialogView.findViewById(R.id.dialog_message).text = context.getString(R.string.dialBtText) + // Set dialog title and message + binding.dialogTitle.text = context.getString(R.string.batteryOpL) + binding.dialogMessage.text = context.getString(R.string.dialBtText) - dialogView.findViewById