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 }