Commit 0b2858d2 authored by Erik Duisters's avatar Erik Duisters Committed by Nicolas Fella

Revamp CustomDevicesActivity

parent e7b9742b
......@@ -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
......
<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>
<?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>
\ No newline at end of file
<?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>
\ No newline at end of file
<?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>
\ No newline at end of file
<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>
<?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() {}
......
/*
* 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);
}
}
/*
* 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();
}
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment