File indexing completed on 2024-12-22 04:41:42
0001 /* 0002 * SPDX-FileCopyrightText: 2014 Achilleas Koutsou <achilleas.k@gmail.com> 0003 * SPDX-FileCopyrightText: 2019 Erik Duisters <e.duisters1@gmail.com> 0004 * 0005 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0006 */ 0007 0008 package org.kde.kdeconnect.UserInterface; 0009 0010 import android.content.SharedPreferences; 0011 import android.os.Bundle; 0012 import android.preference.PreferenceManager; 0013 import android.text.TextUtils; 0014 import android.view.View; 0015 import android.widget.TextView; 0016 0017 import androidx.annotation.NonNull; 0018 import androidx.appcompat.app.AppCompatActivity; 0019 import androidx.appcompat.widget.TooltipCompat; 0020 import androidx.recyclerview.widget.DividerItemDecoration; 0021 import androidx.recyclerview.widget.LinearLayoutManager; 0022 import androidx.recyclerview.widget.RecyclerView; 0023 0024 import com.google.android.material.floatingactionbutton.FloatingActionButton; 0025 import com.google.android.material.snackbar.BaseTransientBottomBar; 0026 import com.google.android.material.snackbar.Snackbar; 0027 0028 import org.kde.kdeconnect_tp.R; 0029 import org.kde.kdeconnect_tp.databinding.ActivityCustomDevicesBinding; 0030 0031 import java.util.ArrayList; 0032 import java.util.Collections; 0033 import java.util.Objects; 0034 0035 //TODO: Require wifi connection so entries can be verified 0036 //TODO: Resolve to ip address and don't allow unresolvable or duplicates based on ip address 0037 //TODO: Sort the list 0038 public class CustomDevicesActivity extends AppCompatActivity implements CustomDevicesAdapter.Callback { 0039 private static final String TAG_ADD_DEVICE_DIALOG = "AddDeviceDialog"; 0040 0041 private static final String KEY_CUSTOM_DEVLIST_PREFERENCE = "device_list_preference"; 0042 private static final String IP_DELIM = ","; 0043 private static final String KEY_EDITING_DEVICE_AT_POSITION = "EditingDeviceAtPosition"; 0044 0045 private RecyclerView recyclerView; 0046 private TextView emptyListMessage; 0047 0048 private ArrayList<String> customDeviceList; 0049 private EditTextAlertDialogFragment addDeviceDialog; 0050 private SharedPreferences sharedPreferences; 0051 private CustomDevicesAdapter customDevicesAdapter; 0052 private DeletedCustomDevice lastDeletedCustomDevice; 0053 private int editingDeviceAtPosition; 0054 0055 @Override 0056 protected void onCreate(Bundle savedInstanceState) { 0057 super.onCreate(savedInstanceState); 0058 0059 final ActivityCustomDevicesBinding binding = ActivityCustomDevicesBinding.inflate(getLayoutInflater()); 0060 setContentView(binding.getRoot()); 0061 0062 recyclerView = binding.recyclerView; 0063 emptyListMessage = binding.emptyListMessage; 0064 final FloatingActionButton fab = binding.floatingActionButton; 0065 0066 setSupportActionBar(binding.toolbarLayout.toolbar); 0067 Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); 0068 getSupportActionBar().setDisplayShowHomeEnabled(true); 0069 0070 fab.setOnClickListener(v -> showEditTextDialog("")); 0071 0072 sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); 0073 0074 customDeviceList = getCustomDeviceList(sharedPreferences); 0075 0076 showEmptyListMessageIfRequired(); 0077 0078 customDevicesAdapter = new CustomDevicesAdapter(this); 0079 customDevicesAdapter.setCustomDevices(customDeviceList); 0080 0081 recyclerView.setHasFixedSize(true); 0082 recyclerView.setLayoutManager(new LinearLayoutManager(this, RecyclerView.VERTICAL, false)); 0083 recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); 0084 recyclerView.setAdapter(customDevicesAdapter); 0085 0086 addDeviceDialog = (EditTextAlertDialogFragment) getSupportFragmentManager().findFragmentByTag(TAG_ADD_DEVICE_DIALOG); 0087 if (addDeviceDialog != null) { 0088 addDeviceDialog.setCallback(new AddDeviceDialogCallback()); 0089 } 0090 0091 TooltipCompat.setTooltipText(fab, getString(R.string.custom_device_fab_hint)); 0092 0093 if (savedInstanceState != null) { 0094 editingDeviceAtPosition = savedInstanceState.getInt(KEY_EDITING_DEVICE_AT_POSITION); 0095 } else { 0096 editingDeviceAtPosition = -1; 0097 } 0098 } 0099 0100 @Override 0101 protected void onSaveInstanceState(@NonNull Bundle outState) { 0102 super.onSaveInstanceState(outState); 0103 0104 outState.putInt(KEY_EDITING_DEVICE_AT_POSITION, editingDeviceAtPosition); 0105 } 0106 0107 private void showEmptyListMessageIfRequired() { 0108 emptyListMessage.setVisibility(customDeviceList.isEmpty() ? View.VISIBLE : View.GONE); 0109 } 0110 0111 private void showEditTextDialog(@NonNull String text) { 0112 addDeviceDialog = new EditTextAlertDialogFragment.Builder() 0113 .setTitle(R.string.add_device_dialog_title) 0114 .setHint(R.string.add_device_hint) 0115 .setText(text) 0116 .setPositiveButton(R.string.ok) 0117 .setNegativeButton(R.string.cancel) 0118 .create(); 0119 0120 addDeviceDialog.setCallback(new AddDeviceDialogCallback()); 0121 addDeviceDialog.show(getSupportFragmentManager(), TAG_ADD_DEVICE_DIALOG); 0122 } 0123 0124 private void saveList() { 0125 String serialized = TextUtils.join(IP_DELIM, customDeviceList); 0126 sharedPreferences 0127 .edit() 0128 .putString(KEY_CUSTOM_DEVLIST_PREFERENCE, serialized) 0129 .apply(); 0130 } 0131 0132 private static ArrayList<String> deserializeIpList(String serialized) { 0133 ArrayList<String> ipList = new ArrayList<>(); 0134 0135 if (!serialized.isEmpty()) { 0136 Collections.addAll(ipList, serialized.split(IP_DELIM)); 0137 } 0138 0139 return ipList; 0140 } 0141 0142 public static ArrayList<String> getCustomDeviceList(SharedPreferences sharedPreferences) { 0143 String deviceListPrefs = sharedPreferences.getString(KEY_CUSTOM_DEVLIST_PREFERENCE, ""); 0144 0145 return deserializeIpList(deviceListPrefs); 0146 } 0147 0148 @Override 0149 public void onCustomDeviceClicked(String customDevice) { 0150 editingDeviceAtPosition = customDeviceList.indexOf(customDevice); 0151 showEditTextDialog(customDevice); 0152 } 0153 0154 @Override 0155 public void onCustomDeviceDismissed(String customDevice) { 0156 lastDeletedCustomDevice = new DeletedCustomDevice(customDevice, customDeviceList.indexOf(customDevice)); 0157 customDeviceList.remove(lastDeletedCustomDevice.position); 0158 customDevicesAdapter.notifyItemRemoved(lastDeletedCustomDevice.position); 0159 saveList(); 0160 showEmptyListMessageIfRequired(); 0161 0162 Snackbar.make(recyclerView, R.string.custom_device_deleted, Snackbar.LENGTH_LONG) 0163 .setAction(R.string.undo, v -> { 0164 customDeviceList.add(lastDeletedCustomDevice.position, lastDeletedCustomDevice.hostnameOrIP); 0165 customDevicesAdapter.notifyItemInserted(lastDeletedCustomDevice.position); 0166 lastDeletedCustomDevice = null; 0167 saveList(); 0168 showEmptyListMessageIfRequired(); 0169 }) 0170 .addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() { 0171 @Override 0172 public void onDismissed(Snackbar transientBottomBar, int event) { 0173 switch (event) { 0174 case DISMISS_EVENT_SWIPE: 0175 case DISMISS_EVENT_TIMEOUT: 0176 lastDeletedCustomDevice = null; 0177 break; 0178 case DISMISS_EVENT_ACTION: 0179 case DISMISS_EVENT_CONSECUTIVE: 0180 case DISMISS_EVENT_MANUAL: 0181 break; 0182 } 0183 } 0184 }) 0185 .show(); 0186 } 0187 0188 private class AddDeviceDialogCallback extends EditTextAlertDialogFragment.Callback { 0189 @Override 0190 public void onPositiveButtonClicked() { 0191 if (addDeviceDialog.editText.getText() != null) { 0192 String deviceNameOrIP = addDeviceDialog.editText.getText().toString().trim(); 0193 0194 // don't add empty string (after trimming) 0195 if (!deviceNameOrIP.isEmpty() && !customDeviceList.contains(deviceNameOrIP)) { 0196 if (editingDeviceAtPosition >= 0) { 0197 customDeviceList.set(editingDeviceAtPosition, deviceNameOrIP); 0198 customDevicesAdapter.notifyItemChanged(editingDeviceAtPosition); 0199 } else { 0200 customDeviceList.add(deviceNameOrIP); 0201 customDevicesAdapter.notifyItemInserted(customDeviceList.size() - 1); 0202 } 0203 0204 saveList(); 0205 showEmptyListMessageIfRequired(); 0206 } 0207 } 0208 } 0209 0210 @Override 0211 public void onDismiss() { 0212 editingDeviceAtPosition = -1; 0213 } 0214 } 0215 0216 private static class DeletedCustomDevice { 0217 @NonNull String hostnameOrIP; 0218 int position; 0219 0220 DeletedCustomDevice(@NonNull String hostnameOrIP, int position) { 0221 this.hostnameOrIP = hostnameOrIP; 0222 this.position = position; 0223 } 0224 } 0225 0226 @Override 0227 public boolean onSupportNavigateUp() { 0228 super.onBackPressed(); 0229 return true; 0230 } 0231 }