diff options
| author | Arne Schwabe <arne@rfc2549.org> | 2025-11-06 07:53:22 +0100 |
|---|---|---|
| committer | Arne Schwabe <arne@rfc2549.org> | 2025-11-06 07:54:02 +0100 |
| commit | dda0a2b1eb3ed42fea4c14fea6806600734d1b41 (patch) | |
| tree | 24036907724ba5920f5d104eba4fac37bc5e3968 /main/src/ui/java | |
| parent | 9aaf03dc355518e7631d6031ec3943002c4f160b (diff) | |
Convert VpnProfileList to Kotlin
Diffstat (limited to 'main/src/ui/java')
| -rw-r--r-- | main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java | 687 | ||||
| -rw-r--r-- | main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.kt | 652 |
2 files changed, 652 insertions, 687 deletions
diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java deleted file mode 100644 index d60051d3..00000000 --- a/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java +++ /dev/null @@ -1,687 +0,0 @@ -/* - * Copyright (c) 2012-2019 Arne Schwabe - * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt - */ - -package de.blinkt.openvpn.fragments; - -import android.Manifest; -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.PersistableBundle; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import androidx.fragment.app.ListFragment; - -import android.text.Html; -import android.text.Html.ImageGetter; -import android.text.SpannableString; -import android.text.SpannableStringBuilder; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.EditText; -import android.widget.ImageButton; -import android.widget.TextView; -import android.widget.Toast; - -import java.util.Collection; -import java.util.Comparator; -import java.util.LinkedList; -import java.util.List; -import java.util.TreeSet; - -import de.blinkt.openvpn.LaunchVPN; -import de.blinkt.openvpn.R; -import de.blinkt.openvpn.VpnProfile; -import de.blinkt.openvpn.activities.ConfigConverter; -import de.blinkt.openvpn.activities.DisconnectVPN; -import de.blinkt.openvpn.activities.FileSelect; -import de.blinkt.openvpn.activities.VPNPreferences; -import de.blinkt.openvpn.core.ConnectionStatus; -import de.blinkt.openvpn.core.PasswordDialogFragment; -import de.blinkt.openvpn.core.Preferences; -import de.blinkt.openvpn.core.ProfileManager; -import de.blinkt.openvpn.core.VpnStatus; - -import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT; -import static de.blinkt.openvpn.core.OpenVPNService.DISCONNECT_VPN; -import static de.blinkt.openvpn.core.OpenVPNService.EXTRA_CHALLENGE_TXT; -import static de.blinkt.openvpn.core.OpenVPNService.EXTRA_START_REASON; - - -public class VPNProfileList extends ListFragment implements OnClickListener, VpnStatus.StateListener { - - public final static int RESULT_VPN_DELETED = Activity.RESULT_FIRST_USER; - public final static int RESULT_VPN_DUPLICATE = Activity.RESULT_FIRST_USER + 1; - // Shortcut version is increased to refresh all shortcuts - final static int SHORTCUT_VERSION = 1; - private static final int MENU_ADD_PROFILE = Menu.FIRST; - private static final int EDIT_VPN_CONFIG = 92; - private static final int SELECT_PROFILE = 43; - private static final int IMPORT_PROFILE = 231; - private static final int FILE_PICKER_RESULT_KITKAT = 392; - private static final int MENU_IMPORT_PROFILE = Menu.FIRST + 1; - private static final int MENU_CHANGE_SORTING = Menu.FIRST + 2; - private static final int MENU_IMPORT_AS = Menu.FIRST + 3; - private static final String PREF_SORT_BY_LRU = "sortProfilesByLRU"; - protected VpnProfile mEditProfile = null; - private String mLastStatusMessage; - private ArrayAdapter<VpnProfile> mArrayadapter; - private Intent mLastIntent; - private VpnProfile defaultVPN; - private View mPermissionView; - private ActivityResultLauncher<String> mPermReceiver; - - @Override - public void updateState(String state, String logmessage, final int localizedResId, ConnectionStatus level, Intent intent) { - requireActivity().runOnUiThread(() -> { - mLastStatusMessage = VpnStatus.getLastCleanLogMessage(getActivity()); - mLastIntent = intent; - mArrayadapter.notifyDataSetChanged(); - showUserRequestDialogIfNeeded(level, intent); - }); - } - - private boolean showUserRequestDialogIfNeeded(ConnectionStatus level, Intent intent) { - if (level == LEVEL_WAITING_FOR_USER_INPUT) { - if (intent != null && intent.getStringExtra(EXTRA_CHALLENGE_TXT) != null) { - PasswordDialogFragment pwInputFrag = PasswordDialogFragment.Companion.newInstance(intent, false); - - pwInputFrag.show(getParentFragmentManager(), "dialog"); - return true; - } - } - return false; - } - - @Override - public void setConnectedVPN(String uuid) { - } - - private void startOrStopVPN(VpnProfile profile) { - if (VpnStatus.isVPNActive() && profile.getUUIDString().equals(VpnStatus.getLastConnectedVPNProfile())) { - if (mLastIntent != null) { - startActivity(mLastIntent); - } else { - Intent disconnectVPN = new Intent(getActivity(), DisconnectVPN.class); - startActivity(disconnectVPN); - } - } else { - startVPN(profile); - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - setListAdapter(); - - registerPermissionReceiver(); - } - - private void registerPermissionReceiver() { - mPermReceiver = registerForActivityResult(new ActivityResultContracts.RequestPermission(), - result -> checkForNotificationPermission(requireView())); - } - - @RequiresApi(api = Build.VERSION_CODES.N_MR1) - void updateDynamicShortcuts() { - PersistableBundle versionExtras = new PersistableBundle(); - versionExtras.putInt("version", SHORTCUT_VERSION); - - ShortcutManager shortcutManager = getContext().getSystemService(ShortcutManager.class); - if (shortcutManager.isRateLimitingActive()) - return; - - List<ShortcutInfo> shortcuts = shortcutManager.getDynamicShortcuts(); - int maxvpn = shortcutManager.getMaxShortcutCountPerActivity() - 1; - - - ShortcutInfo disconnectShortcut = new ShortcutInfo.Builder(getContext(), "disconnectVPN") - .setShortLabel("Disconnect") - .setLongLabel("Disconnect VPN") - .setIntent(new Intent(getContext(), DisconnectVPN.class).setAction(DISCONNECT_VPN)) - .setIcon(Icon.createWithResource(getContext(), R.drawable.ic_shortcut_cancel)) - .setExtras(versionExtras) - .build(); - - LinkedList<ShortcutInfo> newShortcuts = new LinkedList<>(); - LinkedList<ShortcutInfo> updateShortcuts = new LinkedList<>(); - - LinkedList<String> removeShortcuts = new LinkedList<>(); - LinkedList<String> disableShortcuts = new LinkedList<>(); - - boolean addDisconnect = true; - - - TreeSet<VpnProfile> sortedProfilesLRU = new TreeSet<VpnProfile>(new VpnProfileLRUComparator()); - ProfileManager profileManager = ProfileManager.getInstance(getContext()); - sortedProfilesLRU.addAll(profileManager.getProfiles()); - - LinkedList<VpnProfile> LRUProfiles = new LinkedList<>(); - maxvpn = Math.min(maxvpn, sortedProfilesLRU.size()); - - for (int i = 0; i < maxvpn; i++) { - LRUProfiles.add(sortedProfilesLRU.pollFirst()); - } - - for (ShortcutInfo shortcut : shortcuts) { - if (shortcut.getId().equals("disconnectVPN")) { - addDisconnect = false; - if (shortcut.getExtras() == null - || shortcut.getExtras().getInt("version") != SHORTCUT_VERSION) - updateShortcuts.add(disconnectShortcut); - - } else { - VpnProfile p = ProfileManager.get(getContext(), shortcut.getId()); - if (p == null || p.profileDeleted) { - if (shortcut.isEnabled()) { - disableShortcuts.add(shortcut.getId()); - removeShortcuts.add(shortcut.getId()); - } - if (!shortcut.isPinned()) - removeShortcuts.add(shortcut.getId()); - } else { - - if (LRUProfiles.contains(p)) - LRUProfiles.remove(p); - else - removeShortcuts.add(p.getUUIDString()); - - if (!p.getName().equals(shortcut.getShortLabel()) - || shortcut.getExtras() == null - || shortcut.getExtras().getInt("version") != SHORTCUT_VERSION) - updateShortcuts.add(createShortcut(p)); - - - } - - } - - } - if (addDisconnect) - newShortcuts.add(disconnectShortcut); - for (VpnProfile p : LRUProfiles) - newShortcuts.add(createShortcut(p)); - - if (updateShortcuts.size() > 0) - shortcutManager.updateShortcuts(updateShortcuts); - if (removeShortcuts.size() > 0) - shortcutManager.removeDynamicShortcuts(removeShortcuts); - if (newShortcuts.size() > 0) - shortcutManager.addDynamicShortcuts(newShortcuts); - if (disableShortcuts.size() > 0) - shortcutManager.disableShortcuts(disableShortcuts, "VpnProfile does not exist anymore."); - } - - @RequiresApi(Build.VERSION_CODES.N_MR1) - ShortcutInfo createShortcut(VpnProfile profile) { - Intent shortcutIntent = new Intent(Intent.ACTION_MAIN); - shortcutIntent.setClass(requireContext(), LaunchVPN.class); - shortcutIntent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUID().toString()); - shortcutIntent.setAction(Intent.ACTION_MAIN); - shortcutIntent.putExtra(EXTRA_START_REASON, "shortcut"); - shortcutIntent.putExtra("EXTRA_HIDELOG", true); - - PersistableBundle versionExtras = new PersistableBundle(); - versionExtras.putInt("version", SHORTCUT_VERSION); - - return new ShortcutInfo.Builder(getContext(), profile.getUUIDString()) - .setShortLabel(profile.getName()) - .setLongLabel(getString(R.string.qs_connect, profile.getName())) - .setIcon(Icon.createWithResource(getContext(), R.drawable.ic_shortcut_vpn_key)) - .setIntent(shortcutIntent) - .setExtras(versionExtras) - .build(); - } - - @Override - public void onResume() { - super.onResume(); - setListAdapter(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - updateDynamicShortcuts(); - } - VpnStatus.addStateListener(this); - defaultVPN = ProfileManager.getAlwaysOnVPN(requireContext()); - } - - @Override - public void onPause() { - super.onPause(); - VpnStatus.removeStateListener(this); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.vpn_profile_list, container, false); - - TextView newvpntext = (TextView) v.findViewById(R.id.add_new_vpn_hint); - TextView importvpntext = (TextView) v.findViewById(R.id.import_vpn_hint); - - newvpntext.setText(Html.fromHtml(getString(R.string.add_new_vpn_hint), new MiniImageGetter(), null)); - importvpntext.setText(Html.fromHtml(getString(R.string.vpn_import_hint), new MiniImageGetter(), null)); - - ImageButton fab_add = (ImageButton) v.findViewById(R.id.fab_add); - ImageButton fab_import = (ImageButton) v.findViewById(R.id.fab_import); - if (fab_add != null) - fab_add.setOnClickListener(this); - - if (fab_import != null) - fab_import.setOnClickListener(this); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - checkForNotificationPermission(v); - - - return v; - - } - - private void checkForNotificationPermission(View v) { - mPermissionView = v.findViewById(R.id.notification_permission); - boolean permissionGranted = (requireActivity().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED); - mPermissionView.setVisibility(permissionGranted ? View.GONE : View.VISIBLE); - - mPermissionView.setOnClickListener((view) -> { - mPermReceiver.launch(Manifest.permission.POST_NOTIFICATIONS); - }); - } - - private void setListAdapter() { - if (mArrayadapter == null) { - mArrayadapter = new VPNArrayAdapter(getActivity(), R.layout.vpn_list_item, R.id.vpn_item_title); - - } - populateVpnList(); - } - - private void populateVpnList() { - boolean sortByLRU = Preferences.getDefaultSharedPreferences(requireActivity()).getBoolean(PREF_SORT_BY_LRU, false); - getPM().refreshVPNList(requireContext()); - Collection<VpnProfile> allvpn = getPM().getProfiles(); - TreeSet<VpnProfile> sortedset; - if (sortByLRU) - sortedset = new TreeSet<>(new VpnProfileLRUComparator()); - else - sortedset = new TreeSet<>(new VpnProfileNameComparator()); - - sortedset.addAll(allvpn); - mArrayadapter.clear(); - mArrayadapter.addAll(sortedset); - - setListAdapter(mArrayadapter); - mArrayadapter.notifyDataSetChanged(); - } - - @Override - public void onCreateOptionsMenu(Menu menu, @NonNull MenuInflater inflater) { - menu.add(0, MENU_ADD_PROFILE, 0, R.string.menu_add_profile) - .setIcon(R.drawable.ic_menu_add) - .setAlphabeticShortcut('a') - .setTitleCondensed(getActivity().getString(R.string.add)) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - - menu.add(0, MENU_IMPORT_PROFILE, 0, R.string.menu_import) - .setIcon(R.drawable.ic_menu_import) - .setAlphabeticShortcut('i') - .setTitleCondensed(getActivity().getString(R.string.menu_import_short)) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - - menu.add(0, MENU_CHANGE_SORTING, 0, R.string.change_sorting) - .setIcon(R.drawable.ic_sort) - .setAlphabeticShortcut('s') - .setTitleCondensed(getString(R.string.sort)) - .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM); - - menu.add(0, MENU_IMPORT_AS, 0, R.string.import_from_as) - .setIcon(R.drawable.ic_menu_import) - .setAlphabeticShortcut('p') - .setTitleCondensed("Import AS") - .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM); - - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - final int itemId = item.getItemId(); - if (itemId == MENU_ADD_PROFILE) { - onAddOrDuplicateProfile(null); - return true; - } else if (itemId == MENU_IMPORT_PROFILE) { - return startImportConfigFilePicker(); - } else if (itemId == MENU_CHANGE_SORTING) { - return changeSorting(); - } else if (itemId == MENU_IMPORT_AS) { - return startASProfileImport(); - } else { - return super.onOptionsItemSelected(item); - } - } - - private boolean startASProfileImport() { - ImportRemoteConfig asImportFrag = ImportRemoteConfig.newInstance(null); - asImportFrag.show(getParentFragmentManager(), "dialog"); - return true; - } - - private boolean changeSorting() { - SharedPreferences prefs = Preferences.getDefaultSharedPreferences(requireActivity()); - boolean oldValue = prefs.getBoolean(PREF_SORT_BY_LRU, false); - SharedPreferences.Editor prefsedit = prefs.edit(); - if (oldValue) { - Toast.makeText(getActivity(), R.string.sorted_az, Toast.LENGTH_SHORT).show(); - prefsedit.putBoolean(PREF_SORT_BY_LRU, false); - } else { - prefsedit.putBoolean(PREF_SORT_BY_LRU, true); - Toast.makeText(getActivity(), R.string.sorted_lru, Toast.LENGTH_SHORT).show(); - } - prefsedit.apply(); - populateVpnList(); - return true; - } - - @Override - public void onClick(View v) { - switch (v.getId()) { - case R.id.fab_import: - startImportConfigFilePicker(); - break; - case R.id.fab_add: - onAddOrDuplicateProfile(null); - break; - } - } - - private boolean startImportConfigFilePicker() { - boolean startOldFileDialog = true; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !Utils.alwaysUseOldFileChooser(getActivity())) - startOldFileDialog = !startFilePicker(); - - if (startOldFileDialog) - startImportConfig(); - - return true; - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - private boolean startFilePicker() { - - Intent i = Utils.getFilePickerIntent(getActivity(), Utils.FileType.OVPN_CONFIG); - if (i != null) { - startActivityForResult(i, FILE_PICKER_RESULT_KITKAT); - return true; - } else - return false; - } - - private void startImportConfig() { - Intent intent = new Intent(getActivity(), FileSelect.class); - intent.putExtra(FileSelect.NO_INLINE_SELECTION, true); - intent.putExtra(FileSelect.WINDOW_TITLE, R.string.import_configuration_file); - startActivityForResult(intent, SELECT_PROFILE); - } - - private void onAddOrDuplicateProfile(final VpnProfile mCopyProfile) { - Context context = getActivity(); - if (context != null) { - final EditText entry = new EditText(context); - entry.setSingleLine(); - entry.setContentDescription(getString(R.string.name_of_the_vpn_profile)); - - AlertDialog.Builder dialog = new AlertDialog.Builder(context); - if (mCopyProfile == null) - dialog.setTitle(R.string.menu_add_profile); - else { - dialog.setTitle(context.getString(R.string.duplicate_profile_title, mCopyProfile.mName)); - entry.setText(getString(R.string.copy_of_profile, mCopyProfile.mName)); - } - - dialog.setMessage(R.string.add_profile_name_prompt); - dialog.setView(entry); - - dialog.setNeutralButton(R.string.menu_import_short, - (dialog1, which) -> startImportConfigFilePicker()); - dialog.setPositiveButton(android.R.string.ok, - (dialog12, which) -> { - String name = entry.getText().toString(); - if (getPM().getProfileByName(name) == null) { - VpnProfile profile; - if (mCopyProfile != null) { - profile = mCopyProfile.copy(name); - // Remove restrictions on copy profile - profile.mProfileCreator = null; - profile.mUserEditable = true; - } else - profile = new VpnProfile(name); - - addProfile(profile); - editVPN(profile); - } else { - Toast.makeText(getActivity(), R.string.duplicate_profile_name, Toast.LENGTH_LONG).show(); - } - }); - dialog.setNegativeButton(android.R.string.cancel, null); - dialog.create().show(); - } - - } - - private void addProfile(VpnProfile profile) { - getPM().addProfile(profile); - getPM().saveProfileList(getActivity()); - profile.addChangeLogEntry("empty profile added via main profile list"); - getPM().saveProfile(getActivity(), profile); - mArrayadapter.add(profile); - } - - private ProfileManager getPM() { - return ProfileManager.getInstance(getActivity()); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (resultCode == RESULT_VPN_DELETED) { - if (mArrayadapter != null && mEditProfile != null) - mArrayadapter.remove(mEditProfile); - } else if (resultCode == RESULT_VPN_DUPLICATE && data != null) { - String profileUUID = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID); - VpnProfile profile = ProfileManager.get(getActivity(), profileUUID); - if (profile != null) - onAddOrDuplicateProfile(profile); - } - - if (resultCode != Activity.RESULT_OK) - return; - - if (requestCode == EDIT_VPN_CONFIG) { - String configuredVPN = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID); - - VpnProfile profile = ProfileManager.get(getActivity(), configuredVPN); - profile.addChangeLogEntry("Profile edited by user"); - getPM().saveProfile(getActivity(), profile); - // Name could be modified, reset List adapter - setListAdapter(); - - } else if (requestCode == SELECT_PROFILE) { - String fileData = data.getStringExtra(FileSelect.RESULT_DATA); - Uri uri = new Uri.Builder().path(fileData).scheme("file").build(); - - startConfigImport(uri); - } else if (requestCode == IMPORT_PROFILE) { - String profileUUID = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID); - mArrayadapter.add(ProfileManager.get(getActivity(), profileUUID)); - } else if (requestCode == FILE_PICKER_RESULT_KITKAT) { - if (data != null) { - Uri uri = data.getData(); - startConfigImport(uri); - } - } - - } - - private void startConfigImport(Uri uri) { - Intent startImport = new Intent(getActivity(), ConfigConverter.class); - startImport.setAction(ConfigConverter.IMPORT_PROFILE); - startImport.setData(uri); - startActivityForResult(startImport, IMPORT_PROFILE); - } - - private void editVPN(VpnProfile profile) { - mEditProfile = profile; - Intent vprefintent = new Intent(getActivity(), VPNPreferences.class) - .putExtra(getActivity().getPackageName() + ".profileUUID", profile.getUUID().toString()); - - startActivityForResult(vprefintent, EDIT_VPN_CONFIG); - } - - private void startVPN(VpnProfile profile) { - - getPM().saveProfile(getActivity(), profile); - - Intent intent = new Intent(getActivity(), LaunchVPN.class); - intent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUID().toString()); - intent.putExtra(EXTRA_START_REASON, "main profile list"); - intent.setAction(Intent.ACTION_MAIN); - startActivity(intent); - } - - static class VpnProfileNameComparator implements Comparator<VpnProfile> { - - @Override - public int compare(VpnProfile lhs, VpnProfile rhs) { - if (lhs == rhs) - // Catches also both null - return 0; - - if (lhs == null) - return -1; - if (rhs == null) - return 1; - - if (lhs.mName == null) - return -1; - if (rhs.mName == null) - return 1; - - return lhs.mName.compareTo(rhs.mName); - } - - } - - static class VpnProfileLRUComparator implements Comparator<VpnProfile> { - - VpnProfileNameComparator nameComparator = new VpnProfileNameComparator(); - - @Override - public int compare(VpnProfile lhs, VpnProfile rhs) { - if (lhs == rhs) - // Catches also both null - return 0; - - if (lhs == null) - return -1; - if (rhs == null) - return 1; - - // Copied from Long.compare - if (lhs.mLastUsed > rhs.mLastUsed) - return -1; - if (lhs.mLastUsed < rhs.mLastUsed) - return 1; - else - return nameComparator.compare(lhs, rhs); - } - } - - private class VPNArrayAdapter extends ArrayAdapter<VpnProfile> { - - public VPNArrayAdapter(Context context, int resource, - int textViewResourceId) { - super(context, resource, textViewResourceId); - } - - @NonNull - @Override - public View getView(final int position, View convertView, @NonNull ViewGroup parent) { - View v = super.getView(position, convertView, parent); - - final VpnProfile profile = (VpnProfile) getListAdapter().getItem(position); - - View titleview = v.findViewById(R.id.vpn_list_item_left); - titleview.setOnClickListener(v1 -> startOrStopVPN(profile)); - - View settingsview = v.findViewById(R.id.quickedit_settings); - settingsview.setOnClickListener(view -> editVPN(profile)); - - TextView subtitle = v.findViewById(R.id.vpn_item_subtitle); - SpannableStringBuilder warningText = Utils.getWarningText(requireContext(), profile); - - if (profile == defaultVPN) { - if (warningText.length() > 0) - warningText.append(" "); - warningText.append(new SpannableString("Default VPN")); - } - - if (profile.getUUIDString().equals(VpnStatus.getLastConnectedVPNProfile())) { - subtitle.setText(mLastStatusMessage); - subtitle.setVisibility(View.VISIBLE); - } else { - subtitle.setText(warningText); - if (warningText.length() > 0) - subtitle.setVisibility(View.VISIBLE); - else - subtitle.setVisibility(View.GONE); - } - - - return v; - } - } - - class MiniImageGetter implements ImageGetter { - - - @Override - public Drawable getDrawable(String source) { - Drawable d = null; - if ("ic_menu_add".equals(source)) - d = requireActivity().getResources().getDrawable(R.drawable.ic_menu_add_grey, requireActivity().getTheme()); - else if ("ic_menu_archive".equals(source)) - d = requireActivity().getResources().getDrawable(R.drawable.ic_menu_import_grey, requireActivity().getTheme()); - - - if (d != null) { - d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); - return d; - } else { - return null; - } - } - } -} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.kt new file mode 100644 index 00000000..53576c39 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.kt @@ -0,0 +1,652 @@ +/* + * Copyright (c) 2012-2019 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ +package de.blinkt.openvpn.fragments + +import android.Manifest +import android.annotation.TargetApi +import android.app.Activity +import android.app.AlertDialog +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.PersistableBundle +import android.text.Html +import android.text.SpannableString +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.ImageButton +import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts.RequestPermission +import androidx.annotation.RequiresApi +import androidx.fragment.app.ListFragment +import de.blinkt.openvpn.LaunchVPN +import de.blinkt.openvpn.R +import de.blinkt.openvpn.VpnProfile +import de.blinkt.openvpn.activities.ConfigConverter +import de.blinkt.openvpn.activities.DisconnectVPN +import de.blinkt.openvpn.activities.FileSelect +import de.blinkt.openvpn.activities.VPNPreferences +import de.blinkt.openvpn.core.ConnectionStatus +import de.blinkt.openvpn.core.OpenVPNService +import de.blinkt.openvpn.core.PasswordDialogFragment.Companion.newInstance +import de.blinkt.openvpn.core.Preferences +import de.blinkt.openvpn.core.ProfileManager +import de.blinkt.openvpn.core.VpnStatus +import de.blinkt.openvpn.core.VpnStatus.StateListener +import de.blinkt.openvpn.fragments.ImportRemoteConfig.Companion.newInstance +import de.blinkt.openvpn.fragments.Utils.alwaysUseOldFileChooser +import de.blinkt.openvpn.fragments.Utils.getWarningText +import java.util.LinkedList +import java.util.TreeSet +import kotlin.math.min + +class VPNProfileList : ListFragment(), View.OnClickListener, StateListener { + protected var mEditProfile: VpnProfile? = null + private var mLastStatusMessage: String? = null + private var mArrayadapter: ArrayAdapter<VpnProfile>? = null + private var mLastIntent: Intent? = null + private var defaultVPN: VpnProfile? = null + private lateinit var mPermissionView: View + private lateinit var mPermReceiver: ActivityResultLauncher<String> + + override fun updateState( + state: String?, + logmessage: String?, + localizedResId: Int, + level: ConnectionStatus?, + intent: Intent? + ) { + requireActivity().runOnUiThread(Runnable { + mLastStatusMessage = VpnStatus.getLastCleanLogMessage(getActivity()) + mLastIntent = intent + mArrayadapter!!.notifyDataSetChanged() + showUserRequestDialogIfNeeded(level, intent) + }) + } + + private fun showUserRequestDialogIfNeeded(level: ConnectionStatus?, intent: Intent?): Boolean { + if (level == ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT) { + if (intent != null && intent.getStringExtra(OpenVPNService.EXTRA_CHALLENGE_TXT) != null) { + val pwInputFrag = newInstance(intent, false) + + pwInputFrag!!.show(getParentFragmentManager(), "dialog") + return true + } + } + return false + } + + override fun setConnectedVPN(uuid: String?) { + } + + private fun startOrStopVPN(profile: VpnProfile) { + if (VpnStatus.isVPNActive() && profile.getUUIDString() == VpnStatus.getLastConnectedVPNProfile()) { + if (mLastIntent != null) { + startActivity(mLastIntent!!) + } else { + val disconnectVPN = Intent(getActivity(), DisconnectVPN::class.java) + startActivity(disconnectVPN) + } + } else { + startVPN(profile) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + setListAdapter() + + registerPermissionReceiver() + } + + private fun registerPermissionReceiver() { + mPermReceiver = registerForActivityResult<String, Boolean>( + RequestPermission(), + ActivityResultCallback { result: Boolean? -> checkForNotificationPermission(requireView()) }) + } + + @RequiresApi(api = Build.VERSION_CODES.N_MR1) + fun updateDynamicShortcuts() { + val versionExtras = PersistableBundle() + versionExtras.putInt("version", SHORTCUT_VERSION) + + val shortcutManager = + getContext()!!.getSystemService<ShortcutManager>(ShortcutManager::class.java) + if (shortcutManager.isRateLimitingActive()) return + + val shortcuts = shortcutManager.getDynamicShortcuts() + var maxvpn = shortcutManager.getMaxShortcutCountPerActivity() - 1 + + + val disconnectShortcut = ShortcutInfo.Builder(getContext(), "disconnectVPN") + .setShortLabel("Disconnect") + .setLongLabel("Disconnect VPN") + .setIntent( + Intent( + getContext(), + DisconnectVPN::class.java + ).setAction(OpenVPNService.DISCONNECT_VPN) + ) + .setIcon(Icon.createWithResource(getContext(), R.drawable.ic_shortcut_cancel)) + .setExtras(versionExtras) + .build() + + val newShortcuts = LinkedList<ShortcutInfo>() + val updateShortcuts = LinkedList<ShortcutInfo>() + + val removeShortcuts = LinkedList<String>() + val disableShortcuts = LinkedList<String>() + + var addDisconnect = true + + + val sortedProfilesLRU = TreeSet<VpnProfile?>(VpnProfileLRUComparator()) + val profileManager = ProfileManager.getInstance(getContext()) + sortedProfilesLRU.addAll(profileManager.getProfiles()) + + val LRUProfiles = LinkedList<VpnProfile>() + maxvpn = min(maxvpn, sortedProfilesLRU.size) + + for (i in 0..<maxvpn) { + LRUProfiles.add(sortedProfilesLRU.pollFirst()!!) + } + + for (shortcut in shortcuts) { + if (shortcut.getId() == "disconnectVPN") { + addDisconnect = false + if (shortcut.getExtras() == null + || shortcut.getExtras()!!.getInt("version") != SHORTCUT_VERSION + ) updateShortcuts.add(disconnectShortcut) + } else { + val p = ProfileManager.get(getContext(), shortcut.getId()) + if (p == null || p.profileDeleted) { + if (shortcut.isEnabled()) { + disableShortcuts.add(shortcut.getId()) + removeShortcuts.add(shortcut.getId()) + } + if (!shortcut.isPinned()) removeShortcuts.add(shortcut.getId()) + } else { + if (LRUProfiles.contains(p)) LRUProfiles.remove(p) + else removeShortcuts.add(p.getUUIDString()) + + if ((p.getName() != shortcut.getShortLabel()) || shortcut.getExtras() == null || shortcut.getExtras()!! + .getInt("version") != SHORTCUT_VERSION + ) updateShortcuts.add(createShortcut(p)) + } + } + } + if (addDisconnect) newShortcuts.add(disconnectShortcut) + for (p in LRUProfiles) newShortcuts.add(createShortcut(p)) + + if (updateShortcuts.size > 0) shortcutManager.updateShortcuts(updateShortcuts) + if (removeShortcuts.size > 0) shortcutManager.removeDynamicShortcuts(removeShortcuts) + if (newShortcuts.size > 0) shortcutManager.addDynamicShortcuts(newShortcuts) + if (disableShortcuts.size > 0) shortcutManager.disableShortcuts( + disableShortcuts, + "VpnProfile does not exist anymore." + ) + } + + @RequiresApi(Build.VERSION_CODES.N_MR1) + fun createShortcut(profile: VpnProfile): ShortcutInfo { + val shortcutIntent = Intent(Intent.ACTION_MAIN) + shortcutIntent.setClass(requireContext(), LaunchVPN::class.java) + shortcutIntent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUID().toString()) + shortcutIntent.setAction(Intent.ACTION_MAIN) + shortcutIntent.putExtra(OpenVPNService.EXTRA_START_REASON, "shortcut") + shortcutIntent.putExtra("EXTRA_HIDELOG", true) + + val versionExtras = PersistableBundle() + versionExtras.putInt("version", SHORTCUT_VERSION) + + return ShortcutInfo.Builder(getContext(), profile.getUUIDString()) + .setShortLabel(profile.getName()) + .setLongLabel(getString(R.string.qs_connect, profile.getName())) + .setIcon(Icon.createWithResource(getContext(), R.drawable.ic_shortcut_vpn_key)) + .setIntent(shortcutIntent) + .setExtras(versionExtras) + .build() + } + + override fun onResume() { + super.onResume() + setListAdapter() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + updateDynamicShortcuts() + } + VpnStatus.addStateListener(this) + defaultVPN = ProfileManager.getAlwaysOnVPN(requireContext()) + } + + override fun onPause() { + super.onPause() + VpnStatus.removeStateListener(this) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val v = inflater.inflate(R.layout.vpn_profile_list, container, false) + + val newvpntext = v.findViewById<View?>(R.id.add_new_vpn_hint) as TextView + val importvpntext = v.findViewById<View?>(R.id.import_vpn_hint) as TextView + + newvpntext.setText( + Html.fromHtml( + getString(R.string.add_new_vpn_hint), + MiniImageGetter(), + null + ) + ) + importvpntext.setText( + Html.fromHtml( + getString(R.string.vpn_import_hint), + MiniImageGetter(), + null + ) + ) + + val fab_add = v.findViewById<View?>(R.id.fab_add) as ImageButton? + val fab_import = v.findViewById<View?>(R.id.fab_import) as ImageButton? + if (fab_add != null) fab_add.setOnClickListener(this) + + if (fab_import != null) fab_import.setOnClickListener(this) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) checkForNotificationPermission(v) + + + return v + } + + private fun checkForNotificationPermission(v: View) { + mPermissionView = v.findViewById<View>(R.id.notification_permission) + val permissionGranted = + (requireActivity().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) + mPermissionView.setVisibility(if (permissionGranted) View.GONE else View.VISIBLE) + + mPermissionView.setOnClickListener(View.OnClickListener { view: View? -> + mPermReceiver.launch( + Manifest.permission.POST_NOTIFICATIONS + ) + }) + } + + private fun setListAdapter() { + if (mArrayadapter == null) { + mArrayadapter = + VPNArrayAdapter(getActivity()!!, R.layout.vpn_list_item, R.id.vpn_item_title) + } + populateVpnList() + } + + private fun populateVpnList() { + val sortByLRU = Preferences.getDefaultSharedPreferences(requireActivity()).getBoolean( + PREF_SORT_BY_LRU, false + ) + this.pM.refreshVPNList(requireContext()) + val allvpn: MutableCollection<VpnProfile?>? = this.pM.getProfiles() + val sortedset: TreeSet<VpnProfile?>? + if (sortByLRU) sortedset = TreeSet<VpnProfile?>(VpnProfileLRUComparator()) + else sortedset = TreeSet<VpnProfile?>(VpnProfileNameComparator()) + + sortedset.addAll(allvpn!!) + mArrayadapter!!.clear() + mArrayadapter!!.addAll(sortedset) + + setListAdapter(mArrayadapter) + mArrayadapter!!.notifyDataSetChanged() + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + menu.add(0, MENU_ADD_PROFILE, 0, R.string.menu_add_profile) + .setIcon(R.drawable.ic_menu_add) + .setAlphabeticShortcut('a') + .setTitleCondensed(getActivity()!!.getString(R.string.add)) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + + menu.add(0, MENU_IMPORT_PROFILE, 0, R.string.menu_import) + .setIcon(R.drawable.ic_menu_import) + .setAlphabeticShortcut('i') + .setTitleCondensed(getActivity()!!.getString(R.string.menu_import_short)) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + + menu.add(0, MENU_CHANGE_SORTING, 0, R.string.change_sorting) + .setIcon(R.drawable.ic_sort) + .setAlphabeticShortcut('s') + .setTitleCondensed(getString(R.string.sort)) + .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM) + + menu.add(0, MENU_IMPORT_AS, 0, R.string.import_from_as) + .setIcon(R.drawable.ic_menu_import) + .setAlphabeticShortcut('p') + .setTitleCondensed("Import AS") + .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val itemId = item.getItemId() + if (itemId == MENU_ADD_PROFILE) { + onAddOrDuplicateProfile(null) + return true + } else if (itemId == MENU_IMPORT_PROFILE) { + return startImportConfigFilePicker() + } else if (itemId == MENU_CHANGE_SORTING) { + return changeSorting() + } else if (itemId == MENU_IMPORT_AS) { + return startASProfileImport() + } else { + return super.onOptionsItemSelected(item) + } + } + + private fun startASProfileImport(): Boolean { + val asImportFrag = newInstance(null) + asImportFrag.show(getParentFragmentManager(), "dialog") + return true + } + + private fun changeSorting(): Boolean { + val prefs = Preferences.getDefaultSharedPreferences(requireActivity()) + val oldValue = prefs.getBoolean(PREF_SORT_BY_LRU, false) + val prefsedit = prefs.edit() + if (oldValue) { + Toast.makeText(getActivity(), R.string.sorted_az, Toast.LENGTH_SHORT).show() + prefsedit.putBoolean(PREF_SORT_BY_LRU, false) + } else { + prefsedit.putBoolean(PREF_SORT_BY_LRU, true) + Toast.makeText(getActivity(), R.string.sorted_lru, Toast.LENGTH_SHORT).show() + } + prefsedit.apply() + populateVpnList() + return true + } + + override fun onClick(v: View) { + when (v.getId()) { + R.id.fab_import -> startImportConfigFilePicker() + R.id.fab_add -> onAddOrDuplicateProfile(null) + } + } + + private fun startImportConfigFilePicker(): Boolean { + var startOldFileDialog = true + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !alwaysUseOldFileChooser( + getActivity() + ) + ) startOldFileDialog = !startFilePicker() + + if (startOldFileDialog) startImportConfig() + + return true + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + private fun startFilePicker(): Boolean { + val i = Utils.getFilePickerIntent(getActivity()!!, Utils.FileType.OVPN_CONFIG) + if (i != null) { + startActivityForResult(i, FILE_PICKER_RESULT_KITKAT) + return true + } else return false + } + + private fun startImportConfig() { + val intent = Intent(getActivity(), FileSelect::class.java) + intent.putExtra(FileSelect.NO_INLINE_SELECTION, true) + intent.putExtra(FileSelect.WINDOW_TITLE, R.string.import_configuration_file) + startActivityForResult(intent, SELECT_PROFILE) + } + + private fun onAddOrDuplicateProfile(mCopyProfile: VpnProfile?) { + val context: Context? = getActivity() + if (context != null) { + val entry = EditText(context) + entry.setSingleLine() + entry.setContentDescription(getString(R.string.name_of_the_vpn_profile)) + + val dialog = AlertDialog.Builder(context) + if (mCopyProfile == null) dialog.setTitle(R.string.menu_add_profile) + else { + dialog.setTitle( + context.getString( + R.string.duplicate_profile_title, + mCopyProfile.mName + ) + ) + entry.setText(getString(R.string.copy_of_profile, mCopyProfile.mName)) + } + + dialog.setMessage(R.string.add_profile_name_prompt) + dialog.setView(entry) + + dialog.setNeutralButton( + R.string.menu_import_short + ) { dialog1: DialogInterface?, which: Int -> startImportConfigFilePicker() } + dialog.setPositiveButton( + android.R.string.ok + ) { dialog12: DialogInterface?, which: Int -> + val name = entry.getText().toString() + if (this.pM.getProfileByName(name) == null) { + val profile: VpnProfile + if (mCopyProfile != null) { + profile = mCopyProfile.copy(name) + // Remove restrictions on copy profile + profile.mProfileCreator = null + profile.mUserEditable = true + } else profile = VpnProfile(name) + + addProfile(profile) + editVPN(profile) + } else { + Toast.makeText( + getActivity(), + R.string.duplicate_profile_name, + Toast.LENGTH_LONG + ).show() + } + } + dialog.setNegativeButton(android.R.string.cancel, null) + dialog.create().show() + } + } + + private fun addProfile(profile: VpnProfile) { + this.pM.addProfile(profile) + this.pM.saveProfileList(getActivity()) + profile.addChangeLogEntry("empty profile added via main profile list") + ProfileManager.saveProfile(getActivity(), profile) + mArrayadapter!!.add(profile) + } + + private val pM: ProfileManager + get() = ProfileManager.getInstance(getActivity()) + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (resultCode == RESULT_VPN_DELETED) { + if (mArrayadapter != null && mEditProfile != null) mArrayadapter!!.remove(mEditProfile) + } else if (resultCode == RESULT_VPN_DUPLICATE && data != null) { + val profileUUID = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID) + val profile = ProfileManager.get(getActivity(), profileUUID) + if (profile != null) onAddOrDuplicateProfile(profile) + } + + if (resultCode != Activity.RESULT_OK) return + + if (requestCode == EDIT_VPN_CONFIG) { + val configuredVPN = data!!.getStringExtra(VpnProfile.EXTRA_PROFILEUUID) + + val profile = ProfileManager.get(getActivity(), configuredVPN) + profile.addChangeLogEntry("Profile edited by user") + ProfileManager.saveProfile(getActivity(), profile) + // Name could be modified, reset List adapter + setListAdapter() + } else if (requestCode == SELECT_PROFILE) { + val fileData = data!!.getStringExtra(FileSelect.RESULT_DATA) + val uri = Uri.Builder().path(fileData).scheme("file").build() + + startConfigImport(uri) + } else if (requestCode == IMPORT_PROFILE) { + val profileUUID = data!!.getStringExtra(VpnProfile.EXTRA_PROFILEUUID) + mArrayadapter!!.add(ProfileManager.get(getActivity(), profileUUID)) + } else if (requestCode == FILE_PICKER_RESULT_KITKAT) { + if (data != null) { + val uri = data.getData() + startConfigImport(uri) + } + } + } + + private fun startConfigImport(uri: Uri?) { + val startImport = Intent(getActivity(), ConfigConverter::class.java) + startImport.setAction(ConfigConverter.IMPORT_PROFILE) + startImport.setData(uri) + startActivityForResult(startImport, IMPORT_PROFILE) + } + + private fun editVPN(profile: VpnProfile) { + mEditProfile = profile + val vprefintent = Intent(getActivity(), VPNPreferences::class.java) + .putExtra( + getActivity()!!.getPackageName() + ".profileUUID", + profile.getUUID().toString() + ) + + startActivityForResult(vprefintent, EDIT_VPN_CONFIG) + } + + private fun startVPN(profile: VpnProfile) { + ProfileManager.saveProfile(getActivity(), profile) + + val intent = Intent(getActivity(), LaunchVPN::class.java) + intent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUID().toString()) + intent.putExtra(OpenVPNService.EXTRA_START_REASON, "main profile list") + intent.setAction(Intent.ACTION_MAIN) + startActivity(intent) + } + + internal class VpnProfileNameComparator : Comparator<VpnProfile?> { + override fun compare(lhs: VpnProfile?, rhs: VpnProfile?): Int { + if (lhs === rhs) // Catches also both null + return 0 + + if (lhs == null) return -1 + if (rhs == null) return 1 + + if (lhs.mName == null) return -1 + if (rhs.mName == null) return 1 + + return lhs.mName.compareTo(rhs.mName) + } + } + + internal class VpnProfileLRUComparator : Comparator<VpnProfile?> { + var nameComparator: VpnProfileNameComparator = VpnProfileNameComparator() + + override fun compare(lhs: VpnProfile?, rhs: VpnProfile?): Int { + if (lhs === rhs) // Catches also both null + return 0 + + if (lhs == null) return -1 + if (rhs == null) return 1 + + // Copied from Long.compare + if (lhs.mLastUsed > rhs.mLastUsed) return -1 + if (lhs.mLastUsed < rhs.mLastUsed) return 1 + else return nameComparator.compare(lhs, rhs) + } + } + + private inner class VPNArrayAdapter( + context: Context, resource: Int, + textViewResourceId: Int + ) : ArrayAdapter<VpnProfile>(context, resource, textViewResourceId) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val v = super.getView(position, convertView, parent) + + val profile = getListAdapter()!!.getItem(position) as VpnProfile + + val titleview = v.findViewById<View>(R.id.vpn_list_item_left) + titleview.setOnClickListener(View.OnClickListener { v1: View? -> startOrStopVPN(profile) }) + + val settingsview = v.findViewById<View>(R.id.quickedit_settings) + settingsview.setOnClickListener(View.OnClickListener { view: View? -> editVPN(profile) }) + + val subtitle = v.findViewById<TextView>(R.id.vpn_item_subtitle) + val warningText = getWarningText(requireContext(), profile) + + if (profile === defaultVPN) { + if (warningText.length > 0) warningText.append(" ") + warningText.append(SpannableString("Default VPN")) + } + + if (profile.getUUIDString() == VpnStatus.getLastConnectedVPNProfile()) { + subtitle.setText(mLastStatusMessage) + subtitle.setVisibility(View.VISIBLE) + } else { + subtitle.setText(warningText) + if (warningText.length > 0) subtitle.setVisibility(View.VISIBLE) + else subtitle.setVisibility(View.GONE) + } + + + return v + } + } + + internal inner class MiniImageGetter : Html.ImageGetter { + override fun getDrawable(source: String?): Drawable? { + var d: Drawable? = null + if ("ic_menu_add" == source) d = requireActivity().getResources() + .getDrawable(R.drawable.ic_menu_add_grey, requireActivity().getTheme()) + else if ("ic_menu_archive" == source) d = requireActivity().getResources() + .getDrawable(R.drawable.ic_menu_import_grey, requireActivity().getTheme()) + + + if (d != null) { + d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()) + return d + } else { + return null + } + } + } + + companion object { + val RESULT_VPN_DELETED: Int = Activity.RESULT_FIRST_USER + val RESULT_VPN_DUPLICATE: Int = Activity.RESULT_FIRST_USER + 1 + + // Shortcut version is increased to refresh all shortcuts + const val SHORTCUT_VERSION: Int = 1 + private val MENU_ADD_PROFILE = Menu.FIRST + private const val EDIT_VPN_CONFIG = 92 + private const val SELECT_PROFILE = 43 + private const val IMPORT_PROFILE = 231 + private const val FILE_PICKER_RESULT_KITKAT = 392 + private val MENU_IMPORT_PROFILE = Menu.FIRST + 1 + private val MENU_CHANGE_SORTING = Menu.FIRST + 2 + private val MENU_IMPORT_AS = Menu.FIRST + 3 + private const val PREF_SORT_BY_LRU = "sortProfilesByLRU" + } +} |
