2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-31 14:15:14 +00:00

Revamp CustomDevicesActivity

This commit is contained in:
Erik Duisters
2019-02-16 23:58:18 +00:00
committed by Nicolas Fella
parent e7b9742b73
commit 0b2858d222
15 changed files with 660 additions and 140 deletions

View File

@@ -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

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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);

View File

@@ -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() {}

View File

@@ -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;
}
}
}

View 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);
}
}

View File

@@ -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();
}
}
}