diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/UploadNotification.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/UploadNotification.java index dbc47592..14854318 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/UploadNotification.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/UploadNotification.java @@ -1,108 +1,106 @@ /* * Copyright 2019 Erik Duisters * * 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 . */ package org.kde.kdeconnect.Plugins.SharePlugin; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.NotificationHelper; import org.kde.kdeconnect_tp.R; import androidx.core.app.NotificationCompat; import androidx.preference.PreferenceManager; class UploadNotification { private final NotificationManager notificationManager; private NotificationCompat.Builder builder; private final int notificationId; private final Device device; - private long currentJobId; UploadNotification(Device device) { this.device = device; notificationId = (int) System.currentTimeMillis(); notificationManager = (NotificationManager) device.getContext().getSystemService(Context.NOTIFICATION_SERVICE); builder = new NotificationCompat.Builder(device.getContext(), NotificationHelper.Channels.FILETRANSFER) .setSmallIcon(android.R.drawable.stat_sys_upload) .setAutoCancel(true) .setOngoing(true) .setProgress(100, 0, true); } void addCancelAction(long jobId) { builder.mActions.clear(); - currentJobId = jobId; Intent cancelIntent = new Intent(device.getContext(), ShareBroadcastReceiver.class); cancelIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); cancelIntent.setAction(SharePlugin.ACTION_CANCEL_SHARE); cancelIntent.putExtra(SharePlugin.CANCEL_SHARE_BACKGROUND_JOB_ID_EXTRA, jobId); cancelIntent.putExtra(SharePlugin.CANCEL_SHARE_DEVICE_ID_EXTRA, device.getDeviceId()); PendingIntent cancelPendingIntent = PendingIntent.getBroadcast(device.getContext(), 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT); builder.addAction(R.drawable.ic_reject_pairing, device.getContext().getString(R.string.cancel), cancelPendingIntent); } public void setTitle(String title) { builder.setContentTitle(title); builder.setTicker(title); } public void setProgress(int progress, String progressMessage) { builder.setProgress( 100, progress, false); builder.setContentText(progressMessage); builder.setStyle(new NotificationCompat.BigTextStyle().bigText(progressMessage)); } public void setFinished(String message) { builder = new NotificationCompat.Builder(device.getContext(), NotificationHelper.Channels.DEFAULT); builder.setContentTitle(message) .setTicker(message) .setSmallIcon(android.R.drawable.stat_sys_upload_done) .setAutoCancel(true) .setOngoing(false); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(device.getContext()); if (prefs.getBoolean("share_notification_preference", true)) { builder.setDefaults(Notification.DEFAULT_ALL); } } public void setFailed(String message) { setFinished(message); builder.setSmallIcon(android.R.drawable.stat_notify_error); } public void cancel() { notificationManager.cancel(notificationId); } void show() { NotificationHelper.notifyCompat(notificationManager, notificationId, builder.build()); } } diff --git a/src/org/kde/kdeconnect/UserInterface/CustomDevicesAdapter.java b/src/org/kde/kdeconnect/UserInterface/CustomDevicesAdapter.java index 8b300951..a4798b54 100644 --- a/src/org/kde/kdeconnect/UserInterface/CustomDevicesAdapter.java +++ b/src/org/kde/kdeconnect/UserInterface/CustomDevicesAdapter.java @@ -1,181 +1,179 @@ /* * Copyright 2019 Erik Duisters * * 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 . */ 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 { private ArrayList customDevices; - private RecyclerView recyclerView; private final Callback callback; CustomDevicesAdapter(@NonNull Callback callback) { this.callback = callback; customDevices = new ArrayList<>(); } void setCustomDevices(ArrayList 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; 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); } } diff --git a/tests/org/kde/kdeconnect/LanLinkTest.java b/tests/org/kde/kdeconnect/LanLinkTest.java index d51e627a..d1e72e2e 100644 --- a/tests/org/kde/kdeconnect/LanLinkTest.java +++ b/tests/org/kde/kdeconnect/LanLinkTest.java @@ -1,244 +1,243 @@ /* * Copyright 2015 Vineet Garg * * 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 . */ package org.kde.kdeconnect; import android.content.Context; import android.util.Log; import org.json.JSONException; import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.kde.kdeconnect.Backends.LanBackend.LanLink; import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider; import org.mockito.Mockito; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import javax.net.ssl.SSLSocket; import static org.junit.Assert.assertEquals; @RunWith(PowerMockRunner.class) @PrepareForTest({Log.class}) public class LanLinkTest { private LanLink badLanLink; private LanLink goodLanLink; - private OutputStream badOutputStream; private OutputStream goodOutputStream; private Device.SendPacketStatusCallback callback; @Before public void setUp() throws Exception { PowerMockito.mockStatic(Log.class); LanLinkProvider linkProvider = Mockito.mock(LanLinkProvider.class); Mockito.when(linkProvider.getName()).thenReturn("LanLinkProvider"); callback = Mockito.mock(Device.SendPacketStatusCallback.class); goodOutputStream = Mockito.mock(OutputStream.class); - badOutputStream = Mockito.mock(OutputStream.class); + OutputStream badOutputStream = Mockito.mock(OutputStream.class); Mockito.doThrow(new IOException("AAA")).when(badOutputStream).write(Mockito.any(byte[].class)); SSLSocket socketMock = Mockito.mock(SSLSocket.class); Mockito.when(socketMock.getRemoteSocketAddress()).thenReturn(new InetSocketAddress(5000)); Mockito.when(socketMock.getOutputStream()).thenReturn(goodOutputStream); SSLSocket socketBadMock = Mockito.mock(SSLSocket.class); Mockito.when(socketBadMock.getRemoteSocketAddress()).thenReturn(new InetSocketAddress(5000)); Mockito.when(socketBadMock.getOutputStream()).thenReturn(badOutputStream); Context context = Mockito.mock(Context.class); goodLanLink = new LanLink(context, "testDevice", linkProvider, socketMock, LanLink.ConnectionStarted.Remotely); badLanLink = new LanLink(context, "testDevice", linkProvider, socketBadMock, LanLink.ConnectionStarted.Remotely); } @Test public void testSendPacketSuccess() throws JSONException { NetworkPacket testPacket = Mockito.mock(NetworkPacket.class); Mockito.when(testPacket.getType()).thenReturn("kdeconnect.test"); Mockito.when(testPacket.getBoolean("isTesting")).thenReturn(true); Mockito.when(testPacket.getString("testName")).thenReturn("testSendPacketSuccess"); Mockito.when(testPacket.serialize()).thenReturn("{\"id\":123,\"type\":\"kdeconnect.test\",\"body\":{\"isTesting\":true,\"testName\":\"testSendPacketSuccess\"}}"); goodLanLink.sendPacket(testPacket, callback); Mockito.verify(callback).onSuccess(); } @Test public void testSendPacketFail() throws JSONException { NetworkPacket testPacket = Mockito.mock(NetworkPacket.class); Mockito.when(testPacket.getType()).thenReturn("kdeconnect.test"); Mockito.when(testPacket.getBoolean("isTesting")).thenReturn(true); Mockito.when(testPacket.getString("testName")).thenReturn("testSendPacketFail"); Mockito.when(testPacket.serialize()).thenReturn("{\"id\":123,\"type\":\"kdeconnect.test\",\"body\":{\"isTesting\":true,\"testName\":\"testSendPacketFail\"}}"); badLanLink.sendPacket(testPacket, callback); Mockito.verify(callback).onFailure(Mockito.any(IOException.class)); } @Test public void testSendPayload() throws Exception { class Downloader extends Thread { NetworkPacket np; final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); void setNetworkPacket(NetworkPacket networkPacket) { this.np = networkPacket; } ByteArrayOutputStream getOutputStream() { return outputStream; } @Override public void run() { try { Socket socket = null; try { socket = new Socket(); int tcpPort = np.getPayloadTransferInfo().getInt("port"); InetSocketAddress address = new InetSocketAddress(5000); socket.connect(new InetSocketAddress(address.getAddress(), tcpPort)); np.setPayload(new NetworkPacket.Payload(socket.getInputStream(), np.getPayloadSize())); } catch (Exception e) { socket.close(); Log.e("KDE/LanLinkTest", "Exception connecting to remote socket", e); throw e; } final InputStream input = np.getPayload().getInputStream(); final long fileLength = np.getPayloadSize(); byte data[] = new byte[1024]; long progress = 0, prevProgressPercentage = 0; int count; while ((count = input.read(data)) >= 0) { progress += count; outputStream.write(data, 0, count); if (fileLength > 0) { if (progress >= fileLength) break; long progressPercentage = (progress * 100 / fileLength); if (progressPercentage != prevProgressPercentage) { prevProgressPercentage = progressPercentage; } } } outputStream.close(); input.close(); } catch (Exception e) { Log.e("Downloader Test", "Exception", e); } } } final Downloader downloader = new Downloader(); // Using byte array for payload, try to use input stream as used in real device String dataString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + " Cras vel erat et ante fringilla tristique. Sed consequat ligula at interdum " + "rhoncus. Integer semper enim felis, id sodales tellus aliquet eget." + " Sed fringilla ac metus eget dictum. Aliquam euismod non sem sit" + " amet dapibus. Interdum et malesuada fames ac ante ipsum primis " + "in faucibus. Nam et ligula placerat, varius justo eu, convallis " + "lorem. Nam consequat consequat tortor et gravida. Praesent " + "ultricies tortor eget ex elementum gravida. Suspendisse aliquet " + "erat a orci feugiat dignissim."; // reallyLongString contains dataString 16 times String reallyLongString = dataString + dataString; reallyLongString = reallyLongString + reallyLongString; reallyLongString = reallyLongString + reallyLongString; reallyLongString = reallyLongString + reallyLongString; final byte[] data = reallyLongString.getBytes(); final JSONObject sharePacketJson = new JSONObject("{\"id\":123,\"body\":{\"filename\":\"data.txt\"},\"payloadTransferInfo\":{},\"payloadSize\":8720,\"type\":\"kdeconnect.share\"}"); // Mocking share package final NetworkPacket sharePacket = Mockito.mock(NetworkPacket.class); Mockito.when(sharePacket.getType()).thenReturn("kdeconnect.share"); Mockito.when(sharePacket.hasPayload()).thenReturn(true); Mockito.when(sharePacket.hasPayloadTransferInfo()).thenReturn(true); Mockito.doAnswer(invocationOnMock -> sharePacketJson.toString()).when(sharePacket).serialize(); Mockito.when(sharePacket.getPayload()).thenReturn(new NetworkPacket.Payload(new ByteArrayInputStream(data), -1)); Mockito.when(sharePacket.getPayloadSize()).thenReturn((long) data.length); Mockito.doAnswer(invocationOnMock -> sharePacketJson.getJSONObject("payloadTransferInfo")).when(sharePacket).getPayloadTransferInfo(); Mockito.doAnswer(invocationOnMock -> { JSONObject object = (JSONObject) invocationOnMock.getArguments()[0]; sharePacketJson.put("payloadTransferInfo", object); return null; }).when(sharePacket).setPayloadTransferInfo(Mockito.any(JSONObject.class)); Mockito.doAnswer(invocationOnMock -> { Log.e("LanLinkTest", "Write to stream"); String stringNetworkPacket = new String((byte[]) invocationOnMock.getArguments()[0]); final NetworkPacket np = NetworkPacket.unserialize(stringNetworkPacket); downloader.setNetworkPacket(np); downloader.start(); return stringNetworkPacket.length(); }).when(goodOutputStream).write(Mockito.any(byte[].class)); goodLanLink.sendPacket(sharePacket, callback); try { // Wait 1 secs for downloader to finish (if some error, it will continue and assert will fail) downloader.join(1000); } catch (Exception e) { Log.e("Test", "Exception", e); throw e; } assertEquals(new String(data), new String(downloader.getOutputStream().toByteArray())); Mockito.verify(callback).onSuccess(); } }