mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-08-31 14:15:14 +00:00
Revamp CustomDevicesActivity
This commit is contained in:
committed by
Nicolas Fella
parent
e7b9742b73
commit
0b2858d222
@@ -15,6 +15,7 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 28
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
dexOptions {
|
||||
javaMaxHeapSize "2g"
|
||||
@@ -70,6 +71,7 @@ dependencies {
|
||||
implementation 'androidx.media:media:1.0.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'com.jakewharton:disklrucache:2.0.2' //For caching album art bitmaps
|
||||
|
||||
|
9
res/drawable/ic_delete.xml
Normal file
9
res/drawable/ic_delete.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
46
res/layout-v21/custom_device_item.xml
Normal file
46
res/layout-v21/custom_device_item.xml
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorButtonNormal">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/deviceNameOrIPBackdrop"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:drawableEnd="@drawable/ic_delete"
|
||||
android:drawableLeft="@drawable/ic_delete"
|
||||
android:drawableRight="@drawable/ic_delete"
|
||||
android:drawableStart="@drawable/ic_delete"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingRight"
|
||||
android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
|
||||
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingLeft"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/swipeableView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:colorBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/deviceNameOrIP"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?android:attr/listPreferredItemHeightSmall"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingRight"
|
||||
android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
|
||||
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingLeft"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSmall"
|
||||
android:visibility="visible"
|
||||
tools:text="192.168.0.1"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
36
res/layout/activity_custom_devices.xml
Normal file
36
res/layout/activity_custom_devices.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:listitem="@layout/custom_device_item"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyListMessage"
|
||||
style="@style/TextAppearance.AppCompat.Medium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="@dimen/activity_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/activity_horizontal_margin"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/custom_device_list_help"
|
||||
/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/floatingActionButton"
|
||||
style="@style/Widget.MaterialComponents.FloatingActionButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/fab_margin"
|
||||
app:elevation="@dimen/fab_elevation"
|
||||
app:srcCompat="@drawable/ic_add"/>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
42
res/layout/custom_device_item.xml
Normal file
42
res/layout/custom_device_item.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorButtonNormal">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/deviceNameOrIPBackdrop"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingRight"
|
||||
android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
|
||||
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingLeft"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/swipeableView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:colorBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/deviceNameOrIP"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?android:attr/listPreferredItemHeightSmall"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingRight"
|
||||
android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
|
||||
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingLeft"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSmall"
|
||||
android:visibility="visible"
|
||||
tools:text="192.168.0.1"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
@@ -1,35 +0,0 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin">
|
||||
|
||||
<ListView
|
||||
android:id="@android:id/list"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="100dp"
|
||||
android:text="@string/custom_dev_list_help" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/ip_edittext"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/add_host_hint"
|
||||
android:imeOptions="actionSend" />
|
||||
|
||||
<Button
|
||||
android:id="@android:id/button1"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/add_host" />
|
||||
|
||||
|
||||
</LinearLayout>
|
32
res/layout/edit_text_alert_dialog_view.xml
Normal file
32
res/layout/edit_text_alert_dialog_view.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:paddingTop="?dialogPreferredPadding"
|
||||
android:paddingStart="?dialogPreferredPadding"
|
||||
android:paddingLeft="?dialogPreferredPadding"
|
||||
android:paddingEnd="?dialogPreferredPadding"
|
||||
android:paddingRight="?dialogPreferredPadding">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/textInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:hintEnabled="false"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox">
|
||||
|
||||
<!-- inputType="text" is needed, without it lines and maxLines is ignored https://issuetracker.google.com/issues/37118772 -->
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/textInputEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:lines="1"
|
||||
android:maxLines="1"
|
||||
android:inputType="text"
|
||||
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</FrameLayout>
|
@@ -3,4 +3,6 @@
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
<dimen name="key_height">48dip</dimen>
|
||||
<dimen name="fab_margin">16dp</dimen>
|
||||
<dimen name="fab_elevation">6dp</dimen>
|
||||
</resources>
|
||||
|
@@ -210,6 +210,10 @@
|
||||
<string name="unpair_device_action">Unpair %s</string>
|
||||
<string name="custom_device_list">Add devices by IP</string>
|
||||
<string name="delete_custom_device">Delete %s?</string>
|
||||
<string name="custom_device_deleted">Custom device deleted</string>
|
||||
<string name="custom_device_list_help">If your device is not automatically detected you can add its IP address or hostname by clicking on the Floating Action Button</string>
|
||||
<string name="custom_device_fab_hint">Add a device</string>
|
||||
<string name="undo">Undo</string>
|
||||
<string name="share_notification_preference">Noisy notifications</string>
|
||||
<string name="share_notification_preference_summary">Vibrate and play a sound when receiving a file</string>
|
||||
<string name="share_destination_customize">Customize destination directory</string>
|
||||
@@ -226,10 +230,9 @@
|
||||
<string name="sftp_sdcard">SD card</string>
|
||||
<string name="sftp_readonly">(read only)</string>
|
||||
<string name="sftp_camera">Camera pictures</string>
|
||||
<string name="add_host">Add host/IP</string>
|
||||
<string name="add_host_hint">Hostname or IP</string>
|
||||
<string name="add_device_dialog_title">Add device</string>
|
||||
<string name="add_device_hint">Hostname or IP address</string>
|
||||
<string name="no_players_connected">No players found</string>
|
||||
<string name="custom_dev_list_help">Use this option only if your device is not automatically detected. Enter IP address or hostname below and touch the button to add it to the list. Touch an existing item to remove it from the list.</string>
|
||||
<string name="mpris_player_on_device">%1$s on %2$s</string>
|
||||
<string name="send_files">Send files</string>
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<color name="primary">#F67400</color>
|
||||
<color name="primaryDark">#BD5900</color>
|
||||
<color name="accent">#4ebffa</color>
|
||||
<color name="disabled_grey">#eee</color>
|
||||
<color name="disabled_grey">#EEEEEE</color>
|
||||
|
||||
<!-- NoActionBar because we use a Toolbar widget as ActionBar -->
|
||||
<style name="KdeConnectThemeBase" parent="Theme.MaterialComponents.Light.DarkActionBar">
|
||||
|
@@ -383,12 +383,8 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
|
||||
}
|
||||
|
||||
new Thread(() -> {
|
||||
|
||||
String deviceListPrefs = PreferenceManager.getDefaultSharedPreferences(context).getString(CustomDevicesActivity.KEY_CUSTOM_DEVLIST_PREFERENCE, "");
|
||||
ArrayList<String> iplist = new ArrayList<>();
|
||||
if (!deviceListPrefs.isEmpty()) {
|
||||
iplist = CustomDevicesActivity.deserializeIpList(deviceListPrefs);
|
||||
}
|
||||
ArrayList<String> iplist = CustomDevicesActivity
|
||||
.getCustomDeviceList(PreferenceManager.getDefaultSharedPreferences(context));
|
||||
iplist.add("255.255.255.255"); //Default: broadcast.
|
||||
|
||||
NetworkPacket identity = NetworkPacket.createIdentityPacket(context);
|
||||
|
@@ -25,6 +25,7 @@ import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
@@ -37,12 +38,14 @@ public class AlertDialogFragment extends DialogFragment implements DialogInterfa
|
||||
private static final String KEY_MESSAGE_RES_ID = "MessageResId";
|
||||
private static final String KEY_POSITIVE_BUTTON_TEXT_RES_ID = "PositiveButtonResId";
|
||||
private static final String KEY_NEGATIVE_BUTTON_TEXT_RES_ID = "NegativeButtonResId";
|
||||
private static final String KEY_CUSTOM_VIEW_RES_ID = "CustomViewResId";
|
||||
|
||||
@StringRes private int titleResId;
|
||||
@Nullable private String title;
|
||||
@StringRes private int messageResId;
|
||||
@StringRes private int positiveButtonResId;
|
||||
@StringRes private int negativeButtonResId;
|
||||
@LayoutRes private int customViewResId;
|
||||
|
||||
@Nullable private Callback callback;
|
||||
|
||||
@@ -64,6 +67,7 @@ public class AlertDialogFragment extends DialogFragment implements DialogInterfa
|
||||
messageResId = args.getInt(KEY_MESSAGE_RES_ID);
|
||||
positiveButtonResId = args.getInt(KEY_POSITIVE_BUTTON_TEXT_RES_ID);
|
||||
negativeButtonResId = args.getInt(KEY_NEGATIVE_BUTTON_TEXT_RES_ID);
|
||||
customViewResId = args.getInt(KEY_CUSTOM_VIEW_RES_ID);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -72,12 +76,18 @@ public class AlertDialogFragment extends DialogFragment implements DialogInterfa
|
||||
@SuppressLint("ResourceType")
|
||||
String titleString = titleResId > 0 ? getString(titleResId) : title;
|
||||
|
||||
return new AlertDialog.Builder(requireContext())
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext())
|
||||
.setTitle(titleString)
|
||||
.setMessage(messageResId)
|
||||
.setPositiveButton(positiveButtonResId, this)
|
||||
.setNegativeButton(negativeButtonResId, this)
|
||||
.create();
|
||||
.setNegativeButton(negativeButtonResId, this);
|
||||
|
||||
if (customViewResId != 0) {
|
||||
builder.setView(customViewResId);
|
||||
} else {
|
||||
builder.setMessage(messageResId);
|
||||
}
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
public void setCallback(@Nullable Callback callback) {
|
||||
@@ -154,6 +164,11 @@ public class AlertDialogFragment extends DialogFragment implements DialogInterfa
|
||||
return getThis();
|
||||
}
|
||||
|
||||
public B setView(@LayoutRes int customViewResId) {
|
||||
args.putInt(KEY_CUSTOM_VIEW_RES_ID, customViewResId);
|
||||
return getThis();
|
||||
}
|
||||
|
||||
protected abstract F createFragment();
|
||||
|
||||
public F create() {
|
||||
@@ -176,6 +191,7 @@ public class AlertDialogFragment extends DialogFragment implements DialogInterfa
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Generify so the actual AlertDialogFragment subclass can be passed as an argument
|
||||
public static abstract class Callback {
|
||||
public void onPositiveButtonClicked() {}
|
||||
public void onNegativeButtonClicked() {}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright 2014 Achilleas Koutsou <achilleas.k@gmail.com>
|
||||
* Copyright 2019 Erik Duisters <e.duisters1@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
@@ -20,19 +21,16 @@
|
||||
|
||||
package org.kde.kdeconnect.UserInterface;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Build;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
@@ -40,119 +38,133 @@ import org.kde.kdeconnect_tp.R;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import butterknife.Unbinder;
|
||||
|
||||
public class CustomDevicesActivity extends AppCompatActivity {
|
||||
//TODO: Require wifi connection so entries can be verified
|
||||
//TODO: Resolve to ip address and don't allow unresolvable or duplicates based on ip address
|
||||
//TODO: Sort the list
|
||||
public class CustomDevicesActivity extends AppCompatActivity implements CustomDevicesAdapter.Callback {
|
||||
private static final String TAG_ADD_DEVICE_DIALOG = "AddDeviceDialog";
|
||||
|
||||
public static final String KEY_CUSTOM_DEVLIST_PREFERENCE = "device_list_preference";
|
||||
private static final String KEY_CUSTOM_DEVLIST_PREFERENCE = "device_list_preference";
|
||||
private static final String IP_DELIM = ",";
|
||||
private static final String KEY_EDITING_DEVICE_AT_POSITION = "EditingDeviceAtPosition";
|
||||
|
||||
private ListView list;
|
||||
@BindView(R.id.recyclerView) RecyclerView recyclerView;
|
||||
@BindView(R.id.emptyListMessage) TextView emptyListMessage;
|
||||
@BindView(R.id.floatingActionButton) FloatingActionButton fab;
|
||||
|
||||
private ArrayList<String> ipAddressList = new ArrayList<>();
|
||||
private ArrayList<String> customDeviceList;
|
||||
private boolean dialogAlreadyShown = false;
|
||||
private Unbinder unbinder;
|
||||
private EditTextAlertDialogFragment addDeviceDialog;
|
||||
private SharedPreferences sharedPreferences;
|
||||
private CustomDevicesAdapter customDevicesAdapter;
|
||||
private DeletedCustomDevice lastDeletedCustomDevice;
|
||||
private int editingDeviceAtPosition;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
initializeDeviceList(this);
|
||||
ThemeUtil.setUserPreferredTheme(this);
|
||||
setContentView(R.layout.custom_ip_list);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
list = findViewById(android.R.id.list);
|
||||
list.setOnItemClickListener(onClickListener);
|
||||
setContentView(R.layout.activity_custom_devices);
|
||||
|
||||
list.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, ipAddressList));
|
||||
unbinder = ButterKnife.bind(this);
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
|
||||
findViewById(android.R.id.button1).setOnClickListener(v -> addNewDevice());
|
||||
customDeviceList = getCustomDeviceList(sharedPreferences);
|
||||
|
||||
EditText ipEntryBox = findViewById(R.id.ip_edittext);
|
||||
ipEntryBox.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_SEND) {
|
||||
addNewDevice();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
showEmptyListMessageIfRequired();
|
||||
|
||||
customDevicesAdapter = new CustomDevicesAdapter(this);
|
||||
customDevicesAdapter.setCustomDevices(customDeviceList);
|
||||
|
||||
recyclerView.setHasFixedSize(true);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this, RecyclerView.VERTICAL, false));
|
||||
recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
|
||||
recyclerView.setAdapter(customDevicesAdapter);
|
||||
|
||||
addDeviceDialog = (EditTextAlertDialogFragment) getSupportFragmentManager().findFragmentByTag(TAG_ADD_DEVICE_DIALOG);
|
||||
if (addDeviceDialog != null) {
|
||||
addDeviceDialog.setCallback(new AddDeviceDialogCallback());
|
||||
}
|
||||
|
||||
TooltipCompat.setTooltipText(fab, getString(R.string.custom_device_fab_hint));
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
editingDeviceAtPosition = savedInstanceState.getInt(KEY_EDITING_DEVICE_AT_POSITION);
|
||||
} else {
|
||||
editingDeviceAtPosition = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean dialogAlreadyShown = false;
|
||||
private final AdapterView.OnItemClickListener onClickListener = (parent, view, position, id) -> {
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
if (dialogAlreadyShown) {
|
||||
return;
|
||||
}
|
||||
outState.putInt(KEY_EDITING_DEVICE_AT_POSITION, editingDeviceAtPosition);
|
||||
}
|
||||
|
||||
// remove touched item after confirmation
|
||||
DialogInterface.OnClickListener confirmationListener = (dialog, which) -> {
|
||||
switch (which) {
|
||||
case DialogInterface.BUTTON_POSITIVE:
|
||||
ipAddressList.remove(position);
|
||||
saveList();
|
||||
break;
|
||||
case DialogInterface.BUTTON_NEGATIVE:
|
||||
break;
|
||||
}
|
||||
};
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
unbinder.unbind();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(CustomDevicesActivity.this);
|
||||
builder.setMessage(getString(R.string.delete_custom_device, ipAddressList.get(position)));
|
||||
builder.setPositiveButton(R.string.ok, confirmationListener);
|
||||
builder.setNegativeButton(R.string.cancel, confirmationListener);
|
||||
private void showEmptyListMessageIfRequired() {
|
||||
emptyListMessage.setVisibility(customDeviceList.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { //DismissListener
|
||||
dialogAlreadyShown = true;
|
||||
builder.setOnDismissListener(dialog -> dialogAlreadyShown = false);
|
||||
}
|
||||
@OnClick(R.id.floatingActionButton)
|
||||
void onFabClicked() {
|
||||
showEditTextDialog("");
|
||||
}
|
||||
|
||||
builder.show();
|
||||
};
|
||||
private void showEditTextDialog(@NonNull String text) {
|
||||
addDeviceDialog = new EditTextAlertDialogFragment.Builder()
|
||||
.setTitle(R.string.add_device_dialog_title)
|
||||
.setHint(R.string.add_device_hint)
|
||||
.setText(text)
|
||||
.setPositiveButton(R.string.ok)
|
||||
.setNegativeButton(R.string.cancel)
|
||||
.create();
|
||||
|
||||
private void addNewDevice() {
|
||||
EditText ipEntryBox = findViewById(R.id.ip_edittext);
|
||||
String enteredText = ipEntryBox.getText().toString().trim();
|
||||
if (!enteredText.isEmpty()) {
|
||||
// don't add empty string (after trimming)
|
||||
ipAddressList.add(enteredText);
|
||||
}
|
||||
|
||||
saveList();
|
||||
// clear entry box
|
||||
ipEntryBox.setText("");
|
||||
InputMethodManager inputManager = (InputMethodManager)
|
||||
getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
|
||||
View focus = getCurrentFocus();
|
||||
if (focus != null && inputManager != null) {
|
||||
inputManager.hideSoftInputFromWindow(focus.getWindowToken(),
|
||||
InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
}
|
||||
addDeviceDialog.setCallback(new AddDeviceDialogCallback());
|
||||
addDeviceDialog.show(getSupportFragmentManager(), TAG_ADD_DEVICE_DIALOG);
|
||||
}
|
||||
|
||||
private void saveList() {
|
||||
String serialized = TextUtils.join(IP_DELIM, ipAddressList);
|
||||
PreferenceManager.getDefaultSharedPreferences(CustomDevicesActivity.this).edit().putString(
|
||||
KEY_CUSTOM_DEVLIST_PREFERENCE, serialized).apply();
|
||||
((ArrayAdapter) list.getAdapter()).notifyDataSetChanged();
|
||||
|
||||
String serialized = TextUtils.join(IP_DELIM, customDeviceList);
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString(KEY_CUSTOM_DEVLIST_PREFERENCE, serialized)
|
||||
.apply();
|
||||
}
|
||||
|
||||
public static ArrayList<String> deserializeIpList(String serialized) {
|
||||
private static ArrayList<String> deserializeIpList(String serialized) {
|
||||
ArrayList<String> ipList = new ArrayList<>();
|
||||
Collections.addAll(ipList, serialized.split(IP_DELIM));
|
||||
|
||||
if (!serialized.isEmpty()) {
|
||||
Collections.addAll(ipList, serialized.split(IP_DELIM));
|
||||
}
|
||||
|
||||
return ipList;
|
||||
}
|
||||
|
||||
private void initializeDeviceList(Context context) {
|
||||
String deviceListPrefs = PreferenceManager.getDefaultSharedPreferences(context).getString(
|
||||
KEY_CUSTOM_DEVLIST_PREFERENCE, "");
|
||||
if (deviceListPrefs.isEmpty()) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(
|
||||
KEY_CUSTOM_DEVLIST_PREFERENCE,
|
||||
deviceListPrefs).apply();
|
||||
} else {
|
||||
ipAddressList = deserializeIpList(deviceListPrefs);
|
||||
}
|
||||
public static ArrayList<String> getCustomDeviceList(SharedPreferences sharedPreferences) {
|
||||
String deviceListPrefs = sharedPreferences.getString(KEY_CUSTOM_DEVLIST_PREFERENCE, "");
|
||||
|
||||
return deserializeIpList(deviceListPrefs);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -167,4 +179,81 @@ public class CustomDevicesActivity extends AppCompatActivity {
|
||||
BackgroundService.removeGuiInUseCounter(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCustomDeviceClicked(String customDevice) {
|
||||
editingDeviceAtPosition = customDeviceList.indexOf(customDevice);
|
||||
showEditTextDialog(customDevice);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCustomDeviceDismissed(String customDevice) {
|
||||
lastDeletedCustomDevice = new DeletedCustomDevice(customDevice, customDeviceList.indexOf(customDevice));
|
||||
customDeviceList.remove(lastDeletedCustomDevice.position);
|
||||
customDevicesAdapter.notifyItemRemoved(lastDeletedCustomDevice.position);
|
||||
saveList();
|
||||
showEmptyListMessageIfRequired();
|
||||
|
||||
Snackbar.make(recyclerView, R.string.custom_device_deleted, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.undo, v -> {
|
||||
customDeviceList.add(lastDeletedCustomDevice.position, lastDeletedCustomDevice.hostnameOrIP);
|
||||
customDevicesAdapter.notifyItemInserted(lastDeletedCustomDevice.position);
|
||||
lastDeletedCustomDevice = null;
|
||||
saveList();
|
||||
showEmptyListMessageIfRequired();
|
||||
})
|
||||
.addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {
|
||||
@Override
|
||||
public void onDismissed(Snackbar transientBottomBar, int event) {
|
||||
switch (event) {
|
||||
case DISMISS_EVENT_SWIPE:
|
||||
case DISMISS_EVENT_TIMEOUT:
|
||||
lastDeletedCustomDevice = null;
|
||||
break;
|
||||
case DISMISS_EVENT_ACTION:
|
||||
case DISMISS_EVENT_CONSECUTIVE:
|
||||
case DISMISS_EVENT_MANUAL:
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private class AddDeviceDialogCallback extends EditTextAlertDialogFragment.Callback {
|
||||
@Override
|
||||
public void onPositiveButtonClicked() {
|
||||
if (addDeviceDialog.editText.getText() != null) {
|
||||
String deviceNameOrIP = addDeviceDialog.editText.getText().toString().trim();
|
||||
|
||||
// don't add empty string (after trimming)
|
||||
if (!deviceNameOrIP.isEmpty() && !customDeviceList.contains(deviceNameOrIP)) {
|
||||
if (editingDeviceAtPosition >= 0) {
|
||||
customDeviceList.set(editingDeviceAtPosition, deviceNameOrIP);
|
||||
customDevicesAdapter.notifyItemChanged(editingDeviceAtPosition);
|
||||
} else {
|
||||
customDeviceList.add(deviceNameOrIP);
|
||||
customDevicesAdapter.notifyItemInserted(customDeviceList.size() - 1);
|
||||
}
|
||||
|
||||
saveList();
|
||||
showEmptyListMessageIfRequired();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss() {
|
||||
editingDeviceAtPosition = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private class DeletedCustomDevice {
|
||||
@NonNull String hostnameOrIP;
|
||||
int position;
|
||||
|
||||
DeletedCustomDevice(@NonNull String hostnameOrIP, int position) {
|
||||
this.hostnameOrIP = hostnameOrIP;
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
181
src/org/kde/kdeconnect/UserInterface/CustomDevicesAdapter.java
Normal file
181
src/org/kde/kdeconnect/UserInterface/CustomDevicesAdapter.java
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2019 Erik Duisters <e.duisters1@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2 of
|
||||
* the License or (at your option) version 3 or any later version
|
||||
* accepted by the membership of KDE e.V. (or its successor approved
|
||||
* by the membership of KDE e.V.), which shall act as a proxy
|
||||
* defined in Section 14 of version 3 of the license.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.UserInterface;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
|
||||
public class CustomDevicesAdapter extends RecyclerView.Adapter<CustomDevicesAdapter.ViewHolder> {
|
||||
private ArrayList<String> customDevices;
|
||||
private RecyclerView recyclerView;
|
||||
private final Callback callback;
|
||||
|
||||
CustomDevicesAdapter(@NonNull Callback callback) {
|
||||
this.callback = callback;
|
||||
|
||||
customDevices = new ArrayList<>();
|
||||
}
|
||||
|
||||
void setCustomDevices(ArrayList<String> customDevices) {
|
||||
this.customDevices = customDevices;
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView);
|
||||
|
||||
this.recyclerView = recyclerView;
|
||||
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(
|
||||
new ItemTouchHelperCallback(adapterPos -> callback.onCustomDeviceDismissed(customDevices.get(adapterPos))));
|
||||
itemTouchHelper.attachToRecyclerView(recyclerView);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.custom_device_item, parent, false);
|
||||
|
||||
return new ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.bind(customDevices.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return customDevices.size();
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder implements SwipeableViewHolder {
|
||||
@BindView(R.id.deviceNameOrIPBackdrop) TextView deviceNameOrIPBackdrop;
|
||||
@BindView(R.id.swipeableView) FrameLayout swipeableView;
|
||||
@BindView(R.id.deviceNameOrIP) TextView deviceNameOrIP;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
ButterKnife.bind(this, itemView);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
Drawable deleteDrawable = AppCompatResources.getDrawable(itemView.getContext(), R.drawable.ic_delete);
|
||||
deviceNameOrIPBackdrop.setCompoundDrawablesWithIntrinsicBounds(deleteDrawable, null, deleteDrawable, null);
|
||||
}
|
||||
|
||||
deviceNameOrIP.setOnClickListener(v -> callback.onCustomDeviceClicked(customDevices.get(getAdapterPosition())));
|
||||
}
|
||||
|
||||
void bind(String customDevice) {
|
||||
deviceNameOrIP.setText(customDevice);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getSwipeableView() {
|
||||
return swipeableView;
|
||||
}
|
||||
}
|
||||
|
||||
private interface SwipeableViewHolder {
|
||||
View getSwipeableView();
|
||||
}
|
||||
|
||||
private static class ItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
@NonNull private Callback callback;
|
||||
|
||||
private ItemTouchHelperCallback(@NonNull Callback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
return makeMovementFlags(0, ItemTouchHelper.START | ItemTouchHelper.END);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
getDefaultUIUtil().clearView(((SwipeableViewHolder)viewHolder).getSwipeableView());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
|
||||
super.onSelectedChanged(viewHolder, actionState);
|
||||
|
||||
if (viewHolder != null) {
|
||||
getDefaultUIUtil().onSelected(((SwipeableViewHolder) viewHolder).getSwipeableView());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
|
||||
getDefaultUIUtil().onDraw(c, recyclerView, ((SwipeableViewHolder)viewHolder).getSwipeableView(), dX, dY, actionState, isCurrentlyActive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
|
||||
getDefaultUIUtil().onDrawOver(c, recyclerView, ((SwipeableViewHolder)viewHolder).getSwipeableView(), dX, dY, actionState, isCurrentlyActive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
return 0.75f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
callback.onItemDismissed(viewHolder.getAdapterPosition());
|
||||
}
|
||||
|
||||
private interface Callback {
|
||||
void onItemDismissed(int adapterPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onCustomDeviceClicked(String customDevice);
|
||||
void onCustomDeviceDismissed(String customDevice);
|
||||
}
|
||||
}
|
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2019 Erik Duisters <e.duisters1@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2 of
|
||||
* the License or (at your option) version 3 or any later version
|
||||
* accepted by the membership of KDE e.V. (or its successor approved
|
||||
* by the membership of KDE e.V.), which shall act as a proxy
|
||||
* defined in Section 14 of version 3 of the license.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.UserInterface;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
|
||||
public class EditTextAlertDialogFragment extends AlertDialogFragment {
|
||||
private static final String KEY_HINT_RES_ID = "HintResId";
|
||||
private static final String KEY_TEXT = "Text";
|
||||
|
||||
@BindView(R.id.textInputLayout) TextInputLayout textInputLayout;
|
||||
@BindView(R.id.textInputEditText) TextInputEditText editText;
|
||||
private @StringRes int hintResId;
|
||||
private String text;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
Dialog dialog = super.onCreateDialog(savedInstanceState);
|
||||
dialog.setOnShowListener(dialogInterface -> {
|
||||
dialog.setOnShowListener(null);
|
||||
ButterKnife.bind(EditTextAlertDialogFragment.this, dialog);
|
||||
|
||||
textInputLayout.setHintEnabled(true);
|
||||
textInputLayout.setHint(getString(hintResId));
|
||||
editText.setText(text);
|
||||
});
|
||||
|
||||
Bundle args = getArguments();
|
||||
|
||||
if (args != null) {
|
||||
hintResId = args.getInt(KEY_HINT_RES_ID);
|
||||
text = args.getString(KEY_TEXT, "");
|
||||
}
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public static class Builder extends AlertDialogFragment.AbstractBuilder<Builder, EditTextAlertDialogFragment> {
|
||||
public Builder() {
|
||||
super();
|
||||
|
||||
super.setView(R.layout.edit_text_alert_dialog_view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder getThis() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder setView(int customViewResId) {
|
||||
throw new RuntimeException("You cannot set a custom view on an EditTextAlertDialogFragment");
|
||||
}
|
||||
|
||||
public Builder setHint(@StringRes int hintResId) {
|
||||
args.putInt(KEY_HINT_RES_ID, hintResId);
|
||||
return getThis();
|
||||
}
|
||||
|
||||
public Builder setText(@NonNull String text) {
|
||||
args.putString(KEY_TEXT, text);
|
||||
return getThis();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected EditTextAlertDialogFragment createFragment() {
|
||||
return new EditTextAlertDialogFragment();
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user