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 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();
private boolean dialogAlreadyShown = false;
private final AdapterView.OnItemClickListener onClickListener = (parent, view, position, id) -> {
customDevicesAdapter = new CustomDevicesAdapter(this);
customDevicesAdapter.setCustomDevices(customDeviceList);
if (dialogAlreadyShown) {
return;
}
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(this, RecyclerView.VERTICAL, false));
recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
recyclerView.setAdapter(customDevicesAdapter);
// 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;
}
};
addDeviceDialog = (EditTextAlertDialogFragment) getSupportFragmentManager().findFragmentByTag(TAG_ADD_DEVICE_DIALOG);
if (addDeviceDialog != null) {
addDeviceDialog.setCallback(new AddDeviceDialogCallback());
}
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);
TooltipCompat.setTooltipText(fab, getString(R.string.custom_device_fab_hint));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { //DismissListener
dialogAlreadyShown = true;
builder.setOnDismissListener(dialog -> dialogAlreadyShown = false);
if (savedInstanceState != null) {
editingDeviceAtPosition = savedInstanceState.getInt(KEY_EDITING_DEVICE_AT_POSITION);
} else {
editingDeviceAtPosition = -1;
}
}
builder.show();
};
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
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);
}
outState.putInt(KEY_EDITING_DEVICE_AT_POSITION, editingDeviceAtPosition);
}
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);
}
@Override
protected void onDestroy() {
unbinder.unbind();
super.onDestroy();
}
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();
private void showEmptyListMessageIfRequired() {
emptyListMessage.setVisibility(customDeviceList.isEmpty() ? View.VISIBLE : View.GONE);
}
@OnClick(R.id.floatingActionButton)
void onFabClicked() {
showEditTextDialog("");
}
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();
addDeviceDialog.setCallback(new AddDeviceDialogCallback());
addDeviceDialog.show(getSupportFragmentManager(), TAG_ADD_DEVICE_DIALOG);
}
private void saveList() {
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, "");