File indexing completed on 2024-12-22 04:41:42
0001 /* 0002 * SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 package org.kde.kdeconnect.UserInterface; 0008 0009 import android.Manifest; 0010 import android.app.Activity; 0011 import android.content.Context; 0012 import android.content.Intent; 0013 import android.content.pm.PackageManager; 0014 import android.content.res.Resources; 0015 import android.net.Uri; 0016 import android.os.Bundle; 0017 import android.provider.Settings; 0018 import android.view.LayoutInflater; 0019 import android.view.Menu; 0020 import android.view.MenuInflater; 0021 import android.view.MenuItem; 0022 import android.view.View; 0023 import android.view.ViewGroup; 0024 import android.widget.TextView; 0025 0026 import androidx.annotation.NonNull; 0027 import androidx.core.app.ActivityCompat; 0028 import androidx.core.content.ContextCompat; 0029 import androidx.fragment.app.Fragment; 0030 0031 import org.kde.kdeconnect.BackgroundService; 0032 import org.kde.kdeconnect.Device; 0033 import org.kde.kdeconnect.Helpers.TrustedNetworkHelper; 0034 import org.kde.kdeconnect.KdeConnect; 0035 import org.kde.kdeconnect.UserInterface.List.ListAdapter; 0036 import org.kde.kdeconnect.UserInterface.List.PairingDeviceItem; 0037 import org.kde.kdeconnect.UserInterface.List.SectionItem; 0038 import org.kde.kdeconnect_tp.R; 0039 import org.kde.kdeconnect_tp.databinding.DevicesListBinding; 0040 import org.kde.kdeconnect_tp.databinding.PairingExplanationNotTrustedBinding; 0041 import org.kde.kdeconnect_tp.databinding.PairingExplanationTextBinding; 0042 import org.kde.kdeconnect_tp.databinding.PairingExplanationTextNoNotificationsBinding; 0043 import org.kde.kdeconnect_tp.databinding.PairingExplanationTextNoWifiBinding; 0044 0045 import java.util.ArrayList; 0046 import java.util.Collection; 0047 0048 0049 /** 0050 * The view that the user will see when there are no devices paired, or when you choose "add a new device" from the sidebar. 0051 */ 0052 0053 public class PairingFragment extends Fragment implements PairingDeviceItem.Callback { 0054 0055 private static final int RESULT_PAIRING_SUCCESFUL = Activity.RESULT_FIRST_USER; 0056 0057 private DevicesListBinding devicesListBinding; 0058 private PairingExplanationNotTrustedBinding pairingExplanationNotTrustedBinding; 0059 private PairingExplanationTextBinding pairingExplanationTextBinding; 0060 private PairingExplanationTextNoWifiBinding pairingExplanationTextNoWifiBinding; 0061 private PairingExplanationTextNoNotificationsBinding pairingExplanationTextNoNotificationsBinding; 0062 0063 private MainActivity mActivity; 0064 0065 private boolean listRefreshCalledThisFrame = false; 0066 0067 private TextView headerText; 0068 private TextView noWifiHeader; 0069 private TextView noNotificationsHeader; 0070 private TextView notTrustedText; 0071 0072 @Override 0073 public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, 0074 Bundle savedInstanceState) { 0075 mActivity.getSupportActionBar().setTitle(R.string.pairing_title); 0076 0077 setHasOptionsMenu(true); 0078 0079 devicesListBinding = DevicesListBinding.inflate(inflater, container, false); 0080 0081 pairingExplanationNotTrustedBinding = PairingExplanationNotTrustedBinding.inflate(inflater); 0082 notTrustedText = pairingExplanationNotTrustedBinding.getRoot(); 0083 notTrustedText.setOnClickListener(null); 0084 notTrustedText.setOnLongClickListener(null); 0085 0086 pairingExplanationTextBinding = PairingExplanationTextBinding.inflate(inflater); 0087 headerText = pairingExplanationTextBinding.getRoot(); 0088 headerText.setOnClickListener(null); 0089 headerText.setOnLongClickListener(null); 0090 0091 pairingExplanationTextNoWifiBinding = PairingExplanationTextNoWifiBinding.inflate(inflater); 0092 noWifiHeader = pairingExplanationTextNoWifiBinding.getRoot(); 0093 noWifiHeader.setOnClickListener(view -> startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS))); 0094 0095 pairingExplanationTextNoNotificationsBinding = PairingExplanationTextNoNotificationsBinding.inflate(inflater); 0096 noNotificationsHeader = pairingExplanationTextNoNotificationsBinding.getRoot(); 0097 noNotificationsHeader.setOnClickListener(view -> ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.POST_NOTIFICATIONS}, MainActivity.RESULT_NOTIFICATIONS_ENABLED)); 0098 noNotificationsHeader.setOnLongClickListener(view -> { 0099 Intent intent = new Intent(); 0100 intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 0101 Uri uri = Uri.fromParts("package", requireContext().getPackageName(), null); 0102 intent.setData(uri); 0103 startActivity(intent); 0104 return true; 0105 }); 0106 0107 devicesListBinding.devicesList.addHeaderView(headerText); 0108 devicesListBinding.refreshListLayout.setOnRefreshListener(this::refreshDevicesAction); 0109 0110 return devicesListBinding.getRoot(); 0111 } 0112 0113 @Override 0114 public void onDestroyView() { 0115 super.onDestroyView(); 0116 devicesListBinding = null; 0117 pairingExplanationNotTrustedBinding = null; 0118 pairingExplanationTextBinding = null; 0119 pairingExplanationTextNoWifiBinding = null; 0120 } 0121 0122 @Override 0123 public void onAttach(@NonNull Context context) { 0124 super.onAttach(context); 0125 mActivity = ((MainActivity) getActivity()); 0126 } 0127 0128 private void refreshDevicesAction() { 0129 BackgroundService.ForceRefreshConnections(requireContext()); 0130 0131 devicesListBinding.refreshListLayout.setRefreshing(true); 0132 devicesListBinding.refreshListLayout.postDelayed(() -> { 0133 if (devicesListBinding != null) { // the view might be destroyed by now 0134 devicesListBinding.refreshListLayout.setRefreshing(false); 0135 } 0136 }, 1500); 0137 } 0138 0139 private void updateDeviceList() { 0140 if (!isAdded()) { 0141 //Fragment is not attached to an activity. We will crash if we try to do anything here. 0142 return; 0143 } 0144 0145 if (listRefreshCalledThisFrame) { 0146 // This makes sure we don't try to call list.getFirstVisiblePosition() 0147 // twice per frame, because the second time the list hasn't been drawn 0148 // yet and it would always return 0. 0149 return; 0150 } 0151 listRefreshCalledThisFrame = true; 0152 0153 //Check if we're on Wi-Fi/Local network. If we still see a device, don't do anything special 0154 BackgroundService service = BackgroundService.getInstance(); 0155 if (service == null) { 0156 updateConnectivityInfoHeader(true); 0157 } else { 0158 service.isConnectedToNonCellularNetwork().observe(this, this::updateConnectivityInfoHeader); 0159 } 0160 0161 try { 0162 final ArrayList<ListAdapter.Item> items = new ArrayList<>(); 0163 0164 SectionItem connectedSection; 0165 Resources res = getResources(); 0166 0167 connectedSection = new SectionItem(res.getString(R.string.category_connected_devices)); 0168 items.add(connectedSection); 0169 0170 Collection<Device> devices = KdeConnect.getInstance().getDevices().values(); 0171 for (Device device : devices) { 0172 if (device.isReachable() && device.isPaired()) { 0173 items.add(new PairingDeviceItem(device, PairingFragment.this)); 0174 connectedSection.isEmpty = false; 0175 } 0176 } 0177 if (connectedSection.isEmpty) { 0178 items.remove(items.size() - 1); //Remove connected devices section if empty 0179 } 0180 0181 SectionItem availableSection = new SectionItem(res.getString(R.string.category_not_paired_devices)); 0182 items.add(availableSection); 0183 for (Device device : devices) { 0184 if (device.isReachable() && !device.isPaired()) { 0185 items.add(new PairingDeviceItem(device, PairingFragment.this)); 0186 availableSection.isEmpty = false; 0187 } 0188 } 0189 if (availableSection.isEmpty && !connectedSection.isEmpty) { 0190 items.remove(items.size() - 1); //Remove remembered devices section if empty 0191 } 0192 0193 SectionItem rememberedSection = new SectionItem(res.getString(R.string.category_remembered_devices)); 0194 items.add(rememberedSection); 0195 for (Device device : devices) { 0196 if (!device.isReachable() && device.isPaired()) { 0197 items.add(new PairingDeviceItem(device, PairingFragment.this)); 0198 rememberedSection.isEmpty = false; 0199 } 0200 } 0201 if (rememberedSection.isEmpty) { 0202 items.remove(items.size() - 1); //Remove remembered devices section if empty 0203 } 0204 0205 //Store current scroll 0206 int index = devicesListBinding.devicesList.getFirstVisiblePosition(); 0207 View v = devicesListBinding.devicesList.getChildAt(0); 0208 int top = (v == null) ? 0 : (v.getTop() - devicesListBinding.devicesList.getPaddingTop()); 0209 0210 devicesListBinding.devicesList.setAdapter(new ListAdapter(mActivity, items)); 0211 0212 //Restore scroll 0213 devicesListBinding.devicesList.setSelectionFromTop(index, top); 0214 } catch (IllegalStateException e) { 0215 //Ignore: The activity was closed while we were trying to update it 0216 } finally { 0217 listRefreshCalledThisFrame = false; 0218 } 0219 } 0220 0221 void updateConnectivityInfoHeader(boolean isConnectedToNonCellularNetwork) { 0222 Collection<Device> devices = KdeConnect.getInstance().getDevices().values(); 0223 boolean someDevicesReachable = false; 0224 for (Device device : devices) { 0225 if (device.isReachable()) { 0226 someDevicesReachable = true; 0227 } 0228 } 0229 0230 boolean hasNotificationsPermission = ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED; 0231 0232 devicesListBinding.devicesList.removeHeaderView(headerText); 0233 devicesListBinding.devicesList.removeHeaderView(noWifiHeader); 0234 devicesListBinding.devicesList.removeHeaderView(notTrustedText); 0235 devicesListBinding.devicesList.removeHeaderView(noNotificationsHeader); 0236 0237 if (someDevicesReachable || isConnectedToNonCellularNetwork) { 0238 if (!hasNotificationsPermission) { 0239 devicesListBinding.devicesList.addHeaderView(noNotificationsHeader); 0240 } else if (TrustedNetworkHelper.isTrustedNetwork(getContext())) { 0241 devicesListBinding.devicesList.addHeaderView(headerText); 0242 } else { 0243 devicesListBinding.devicesList.addHeaderView(notTrustedText); 0244 } 0245 } else { 0246 devicesListBinding.devicesList.addHeaderView(noWifiHeader); 0247 } 0248 } 0249 @Override 0250 public void onStart() { 0251 super.onStart(); 0252 KdeConnect.getInstance().addDeviceListChangedCallback("PairingFragment", () -> mActivity.runOnUiThread(this::updateDeviceList)); 0253 BackgroundService.ForceRefreshConnections(requireContext()); // force a network re-discover 0254 updateDeviceList(); 0255 } 0256 0257 @Override 0258 public void onStop() { 0259 KdeConnect.getInstance().removeDeviceListChangedCallback("PairingFragment"); 0260 super.onStop(); 0261 } 0262 0263 @Override 0264 public void pairingClicked(Device device) { 0265 mActivity.onDeviceSelected(device.getDeviceId(), !device.isPaired() || !device.isReachable()); 0266 } 0267 0268 @Override 0269 public void onActivityResult(int requestCode, int resultCode, Intent data) { 0270 switch (requestCode) { 0271 case RESULT_PAIRING_SUCCESFUL: 0272 if (resultCode == 1) { 0273 String deviceId = data.getStringExtra("deviceId"); 0274 mActivity.onDeviceSelected(deviceId); 0275 } 0276 break; 0277 default: 0278 super.onActivityResult(requestCode, resultCode, data); 0279 } 0280 } 0281 0282 @Override 0283 public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) { 0284 inflater.inflate(R.menu.pairing, menu); 0285 } 0286 0287 @Override 0288 public boolean onOptionsItemSelected(final MenuItem item) { 0289 int id = item.getItemId(); 0290 if (id == R.id.menu_refresh) { 0291 refreshDevicesAction(); 0292 return true; 0293 } else if (id == R.id.menu_custom_device_list) { 0294 startActivity(new Intent(mActivity, CustomDevicesActivity.class)); 0295 return true; 0296 } else if (id == R.id.menu_trusted_networks) { 0297 startActivity(new Intent(mActivity, TrustedNetworksActivity.class)); 0298 return true; 0299 } else { 0300 return super.onOptionsItemSelected(item); 0301 } 0302 } 0303 0304 0305 }