|
3 | 3 | import android.Manifest; |
4 | 4 | import android.content.Intent; |
5 | 5 | import android.content.SharedPreferences; |
| 6 | +import android.content.DialogInterface; |
6 | 7 | import android.net.VpnService; |
7 | 8 | import android.os.Build; |
8 | 9 | import android.os.Bundle; |
9 | 10 | import android.provider.Settings; |
| 11 | +import android.text.TextUtils; |
10 | 12 | import android.util.Log; |
11 | 13 | import android.view.LayoutInflater; |
12 | 14 | import android.view.View; |
|
23 | 25 | import androidx.recyclerview.widget.RecyclerView; |
24 | 26 | import androidx.viewpager2.widget.ViewPager2; |
25 | 27 |
|
| 28 | +import com.google.android.material.dialog.MaterialAlertDialogBuilder; |
| 29 | + |
26 | 30 | import java.util.ArrayList; |
27 | 31 | import java.util.List; |
28 | 32 |
|
@@ -274,9 +278,29 @@ private void refreshSlides() { |
274 | 278 | slide.warningResId = vpnPrepared ? 0 : R.string.onboarding_vpn_sure; |
275 | 279 | slide.actionListener = v -> { |
276 | 280 | if (!vpnPrepared) { |
277 | | - Intent intent = VpnService.prepare(ActivityOnboarding.this); |
278 | | - if (intent != null) { |
279 | | - startActivityForResult(intent, 0); |
| 281 | + // Proactively detect another Always-on VPN on Android < S, |
| 282 | + // where the setting is still readable. |
| 283 | + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { |
| 284 | + try { |
| 285 | + String alwaysOn = Settings.Secure.getString( |
| 286 | + getContentResolver(), "always_on_vpn_app"); |
| 287 | + if (!TextUtils.isEmpty(alwaysOn) |
| 288 | + && !getPackageName().equals(alwaysOn)) { |
| 289 | + showAlwaysOnVpnBlockedDialog(); |
| 290 | + return; |
| 291 | + } |
| 292 | + } catch (Throwable ex) { |
| 293 | + Log.e("Onboarding", ex.toString()); |
| 294 | + } |
| 295 | + } |
| 296 | + try { |
| 297 | + Intent intent = VpnService.prepare(ActivityOnboarding.this); |
| 298 | + if (intent != null) { |
| 299 | + startActivityForResult(intent, 0); |
| 300 | + } |
| 301 | + } catch (Throwable ex) { |
| 302 | + Log.e("Onboarding", ex.toString()); |
| 303 | + showAlwaysOnVpnBlockedDialog(); |
280 | 304 | } |
281 | 305 | } |
282 | 306 | }; |
@@ -425,6 +449,36 @@ private void updateButtons(int position) { |
425 | 449 | } |
426 | 450 | } |
427 | 451 |
|
| 452 | + @Override |
| 453 | + protected void onActivityResult(int requestCode, int resultCode, Intent data) { |
| 454 | + super.onActivityResult(requestCode, resultCode, data); |
| 455 | + if (requestCode == 0 && resultCode != RESULT_OK) { |
| 456 | + // Consent was denied or silently blocked. If prepare() still returns a |
| 457 | + // non-null intent, Android refused to show the consent dialog — most |
| 458 | + // commonly because another app is set as Always-on VPN. |
| 459 | + if (VpnService.prepare(this) != null) { |
| 460 | + showAlwaysOnVpnBlockedDialog(); |
| 461 | + } |
| 462 | + } |
| 463 | + } |
| 464 | + |
| 465 | + private void showAlwaysOnVpnBlockedDialog() { |
| 466 | + new MaterialAlertDialogBuilder(this) |
| 467 | + .setTitle(R.string.onboarding_vpn_blocked_title) |
| 468 | + .setMessage(android.text.Html.fromHtml( |
| 469 | + getString(R.string.onboarding_vpn_blocked_desc))) |
| 470 | + .setPositiveButton(R.string.onboarding_vpn_blocked_action, |
| 471 | + (DialogInterface.OnClickListener) (dialog, which) -> { |
| 472 | + try { |
| 473 | + startActivity(new Intent(Settings.ACTION_VPN_SETTINGS)); |
| 474 | + } catch (Throwable ex) { |
| 475 | + Log.e("Onboarding", ex.toString()); |
| 476 | + } |
| 477 | + }) |
| 478 | + .setNegativeButton(android.R.string.cancel, null) |
| 479 | + .show(); |
| 480 | + } |
| 481 | + |
428 | 482 | private void finishOnboarding() { |
429 | 483 | boolean vpnPrepared = VpnService.prepare(this) == null; |
430 | 484 |
|
|
0 commit comments