Commit 24067851 by tonihei Committed by Oliver Woodman

Move DownloadManagerTest to Robolectric.

The waiting for a ConditionVariable to be false was replaced by a
CountDownLatch whose count is asserted to be one. The timeout of a
ConditionVariable doesn't work in Robolectric unless someone is
manually increasing the SystemClock time.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=186003125
parent 56c9d023
...@@ -16,16 +16,16 @@ ...@@ -16,16 +16,16 @@
package com.google.android.exoplayer2.offline; package com.google.android.exoplayer2.offline;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.os.ConditionVariable; import android.os.ConditionVariable;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.RobolectricUtil;
import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener; import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener;
import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
import com.google.android.exoplayer2.offline.DownloadManager.DownloadState.State; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState.State;
import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.DummyMainThread;
import com.google.android.exoplayer2.testutil.MockitoUtil;
import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -36,11 +36,22 @@ import java.util.ArrayList; ...@@ -36,11 +36,22 @@ import java.util.ArrayList;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
/** Tests {@link DownloadManager}. */ /** Tests {@link DownloadManager}. */
public class DownloadManagerTest extends InstrumentationTestCase { @RunWith(RobolectricTestRunner.class)
@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})
public class DownloadManagerTest {
/* Used to check if condition becomes true in this time interval. */ /* Used to check if condition becomes true in this time interval. */
private static final int ASSERT_TRUE_TIMEOUT = 10000; private static final int ASSERT_TRUE_TIMEOUT = 10000;
...@@ -56,71 +67,33 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -56,71 +67,33 @@ public class DownloadManagerTest extends InstrumentationTestCase {
private TestDownloadListener testDownloadListener; private TestDownloadListener testDownloadListener;
private DummyMainThread dummyMainThread; private DummyMainThread dummyMainThread;
@Override @Before
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); MockitoAnnotations.initMocks(this);
MockitoUtil.setUpMockito(this);
dummyMainThread = new DummyMainThread(); dummyMainThread = new DummyMainThread();
actionFile = Util.createTempFile(getInstrumentation().getContext(), "ExoPlayerTest"); actionFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest");
testDownloadListener = new TestDownloadListener(); testDownloadListener = new TestDownloadListener();
setUpDownloadManager(100); setUpDownloadManager(100);
} }
@Override @After
public void tearDown() throws Exception { public void tearDown() throws Exception {
releaseDownloadManager(); releaseDownloadManager();
actionFile.delete(); actionFile.delete();
dummyMainThread.release(); dummyMainThread.release();
super.tearDown();
}
private void setUpDownloadManager(final int maxActiveDownloadTasks) throws Exception {
if (downloadManager != null) {
releaseDownloadManager();
}
try {
runOnMainThread(
new Runnable() {
@Override
public void run() {
downloadManager =
new DownloadManager(
new DownloaderConstructorHelper(
Mockito.mock(Cache.class), DummyDataSource.FACTORY),
maxActiveDownloadTasks,
MIN_RETRY_COUNT,
actionFile.getAbsolutePath());
downloadManager.addListener(testDownloadListener);
downloadManager.startDownloads();
}
});
} catch (Throwable throwable) {
throw new Exception(throwable);
}
}
private void releaseDownloadManager() throws Exception {
try {
runOnMainThread(
new Runnable() {
@Override
public void run() {
downloadManager.release();
}
});
} catch (Throwable throwable) {
throw new Exception(throwable);
}
} }
@Test
public void testDownloadActionRuns() throws Throwable { public void testDownloadActionRuns() throws Throwable {
doTestActionRuns(createDownloadAction("media 1")); doTestActionRuns(createDownloadAction("media 1"));
} }
@Test
public void testRemoveActionRuns() throws Throwable { public void testRemoveActionRuns() throws Throwable {
doTestActionRuns(createRemoveAction("media 1")); doTestActionRuns(createRemoveAction("media 1"));
} }
@Test
public void testDownloadRetriesThenFails() throws Throwable { public void testDownloadRetriesThenFails() throws Throwable {
FakeDownloadAction downloadAction = createDownloadAction("media 1"); FakeDownloadAction downloadAction = createDownloadAction("media 1");
downloadAction.post(); downloadAction.post();
...@@ -135,6 +108,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -135,6 +108,7 @@ public class DownloadManagerTest extends InstrumentationTestCase {
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test
public void testDownloadNoRetryWhenCancelled() throws Throwable { public void testDownloadNoRetryWhenCancelled() throws Throwable {
FakeDownloadAction downloadAction = createDownloadAction("media 1").ignoreInterrupts(); FakeDownloadAction downloadAction = createDownloadAction("media 1").ignoreInterrupts();
downloadAction.getFakeDownloader().enableDownloadIOException = true; downloadAction.getFakeDownloader().enableDownloadIOException = true;
...@@ -148,6 +122,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -148,6 +122,7 @@ public class DownloadManagerTest extends InstrumentationTestCase {
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test
public void testDownloadRetriesThenContinues() throws Throwable { public void testDownloadRetriesThenContinues() throws Throwable {
FakeDownloadAction downloadAction = createDownloadAction("media 1"); FakeDownloadAction downloadAction = createDownloadAction("media 1");
downloadAction.post(); downloadAction.post();
...@@ -165,6 +140,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -165,6 +140,7 @@ public class DownloadManagerTest extends InstrumentationTestCase {
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test
@SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"}) @SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"})
public void testDownloadRetryCountResetsOnProgress() throws Throwable { public void testDownloadRetryCountResetsOnProgress() throws Throwable {
FakeDownloadAction downloadAction = createDownloadAction("media 1"); FakeDownloadAction downloadAction = createDownloadAction("media 1");
...@@ -185,36 +161,37 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -185,36 +161,37 @@ public class DownloadManagerTest extends InstrumentationTestCase {
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test
public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable { public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable {
doTestActionsRunInParallel(createDownloadAction("media 1"), doTestActionsRunInParallel(createDownloadAction("media 1"), createDownloadAction("media 2"));
createDownloadAction("media 2"));
} }
@Test
public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable { public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable {
doTestActionsRunInParallel(createDownloadAction("media 1"), doTestActionsRunInParallel(createDownloadAction("media 1"), createRemoveAction("media 2"));
createRemoveAction("media 2"));
} }
@Test
public void testSameMediaDownloadActionsStartInParallel() throws Throwable { public void testSameMediaDownloadActionsStartInParallel() throws Throwable {
doTestActionsRunInParallel(createDownloadAction("media 1"), doTestActionsRunInParallel(createDownloadAction("media 1"), createDownloadAction("media 1"));
createDownloadAction("media 1"));
} }
@Test
public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable { public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable {
doTestActionsRunSequentially(createDownloadAction("media 1"), doTestActionsRunSequentially(createDownloadAction("media 1"), createRemoveAction("media 1"));
createRemoveAction("media 1"));
} }
@Test
public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable { public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable {
doTestActionsRunSequentially(createRemoveAction("media 1"), doTestActionsRunSequentially(createRemoveAction("media 1"), createDownloadAction("media 1"));
createDownloadAction("media 1"));
} }
@Test
public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable { public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable {
doTestActionsRunSequentially(createRemoveAction("media 1"), doTestActionsRunSequentially(createRemoveAction("media 1"), createRemoveAction("media 1"));
createRemoveAction("media 1"));
} }
@Test
public void testSameMediaMultipleActions() throws Throwable { public void testSameMediaMultipleActions() throws Throwable {
FakeDownloadAction downloadAction1 = createDownloadAction("media 1").ignoreInterrupts(); FakeDownloadAction downloadAction1 = createDownloadAction("media 1").ignoreInterrupts();
FakeDownloadAction downloadAction2 = createDownloadAction("media 1").ignoreInterrupts(); FakeDownloadAction downloadAction2 = createDownloadAction("media 1").ignoreInterrupts();
...@@ -250,6 +227,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -250,6 +227,7 @@ public class DownloadManagerTest extends InstrumentationTestCase {
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test
public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable { public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable {
FakeDownloadAction removeAction1 = createRemoveAction("media 1").ignoreInterrupts(); FakeDownloadAction removeAction1 = createRemoveAction("media 1").ignoreInterrupts();
FakeDownloadAction removeAction2 = createRemoveAction("media 1"); FakeDownloadAction removeAction2 = createRemoveAction("media 1");
...@@ -267,6 +245,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -267,6 +245,7 @@ public class DownloadManagerTest extends InstrumentationTestCase {
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test
public void testGetTasks() throws Throwable { public void testGetTasks() throws Throwable {
FakeDownloadAction removeAction = createRemoveAction("media 1"); FakeDownloadAction removeAction = createRemoveAction("media 1");
FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); FakeDownloadAction downloadAction1 = createDownloadAction("media 1");
...@@ -283,6 +262,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -283,6 +262,7 @@ public class DownloadManagerTest extends InstrumentationTestCase {
assertThat(states[2].downloadAction).isEqualTo(downloadAction2); assertThat(states[2].downloadAction).isEqualTo(downloadAction2);
} }
@Test
public void testMultipleWaitingDownloadActionStartsInParallel() throws Throwable { public void testMultipleWaitingDownloadActionStartsInParallel() throws Throwable {
FakeDownloadAction removeAction = createRemoveAction("media 1"); FakeDownloadAction removeAction = createRemoveAction("media 1");
FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); FakeDownloadAction downloadAction1 = createDownloadAction("media 1");
...@@ -301,6 +281,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -301,6 +281,7 @@ public class DownloadManagerTest extends InstrumentationTestCase {
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test
public void testDifferentMediaDownloadActionsPreserveOrder() throws Throwable { public void testDifferentMediaDownloadActionsPreserveOrder() throws Throwable {
FakeDownloadAction removeAction = createRemoveAction("media 1").ignoreInterrupts(); FakeDownloadAction removeAction = createRemoveAction("media 1").ignoreInterrupts();
FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); FakeDownloadAction downloadAction1 = createDownloadAction("media 1");
...@@ -319,6 +300,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -319,6 +300,7 @@ public class DownloadManagerTest extends InstrumentationTestCase {
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test
public void testDifferentMediaRemoveActionsDoNotPreserveOrder() throws Throwable { public void testDifferentMediaRemoveActionsDoNotPreserveOrder() throws Throwable {
FakeDownloadAction downloadAction = createDownloadAction("media 1").ignoreInterrupts(); FakeDownloadAction downloadAction = createDownloadAction("media 1").ignoreInterrupts();
FakeDownloadAction removeAction1 = createRemoveAction("media 1"); FakeDownloadAction removeAction1 = createRemoveAction("media 1");
...@@ -337,6 +319,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -337,6 +319,7 @@ public class DownloadManagerTest extends InstrumentationTestCase {
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test
public void testStopAndResume() throws Throwable { public void testStopAndResume() throws Throwable {
FakeDownloadAction download1Action = createDownloadAction("media 1"); FakeDownloadAction download1Action = createDownloadAction("media 1");
FakeDownloadAction remove2Action = createRemoveAction("media 2"); FakeDownloadAction remove2Action = createRemoveAction("media 2");
...@@ -385,6 +368,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -385,6 +368,7 @@ public class DownloadManagerTest extends InstrumentationTestCase {
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test
public void testResumeBeforeTotallyStopped() throws Throwable { public void testResumeBeforeTotallyStopped() throws Throwable {
setUpDownloadManager(2); setUpDownloadManager(2);
FakeDownloadAction download1Action = createDownloadAction("media 1").ignoreInterrupts(); FakeDownloadAction download1Action = createDownloadAction("media 1").ignoreInterrupts();
...@@ -431,13 +415,52 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -431,13 +415,52 @@ public class DownloadManagerTest extends InstrumentationTestCase {
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
private void setUpDownloadManager(final int maxActiveDownloadTasks) throws Exception {
if (downloadManager != null) {
releaseDownloadManager();
}
try {
runOnMainThread(
new Runnable() {
@Override
public void run() {
downloadManager =
new DownloadManager(
new DownloaderConstructorHelper(
Mockito.mock(Cache.class), DummyDataSource.FACTORY),
maxActiveDownloadTasks,
MIN_RETRY_COUNT,
actionFile.getAbsolutePath());
downloadManager.addListener(testDownloadListener);
downloadManager.startDownloads();
}
});
} catch (Throwable throwable) {
throw new Exception(throwable);
}
}
private void releaseDownloadManager() throws Exception {
try {
runOnMainThread(
new Runnable() {
@Override
public void run() {
downloadManager.release();
}
});
} catch (Throwable throwable) {
throw new Exception(throwable);
}
}
private void doTestActionRuns(FakeDownloadAction action) throws Throwable { private void doTestActionRuns(FakeDownloadAction action) throws Throwable {
action.post().assertStarted().unblock().assertEnded(); action.post().assertStarted().unblock().assertEnded();
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
private void doTestActionsRunSequentially(FakeDownloadAction action1, private void doTestActionsRunSequentially(FakeDownloadAction action1, FakeDownloadAction action2)
FakeDownloadAction action2) throws Throwable { throws Throwable {
action1.ignoreInterrupts().post().assertStarted(); action1.ignoreInterrupts().post().assertStarted();
action2.post().assertDoesNotStart(); action2.post().assertDoesNotStart();
...@@ -448,8 +471,8 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -448,8 +471,8 @@ public class DownloadManagerTest extends InstrumentationTestCase {
testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
private void doTestActionsRunInParallel(FakeDownloadAction action1, private void doTestActionsRunInParallel(FakeDownloadAction action1, FakeDownloadAction action2)
FakeDownloadAction action2) throws Throwable { throws Throwable {
action1.post().assertStarted(); action1.post().assertStarted();
action2.post().assertStarted(); action2.post().assertStarted();
action1.unblock().assertEnded(); action1.unblock().assertEnded();
...@@ -502,7 +525,6 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -502,7 +525,6 @@ public class DownloadManagerTest extends InstrumentationTestCase {
throw new Exception(downloadError); throw new Exception(downloadError);
} }
} }
} }
private class FakeDownloadAction extends DownloadAction { private class FakeDownloadAction extends DownloadAction {
...@@ -561,12 +583,13 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -561,12 +583,13 @@ public class DownloadManagerTest extends InstrumentationTestCase {
return this; return this;
} }
private FakeDownloadAction assertDoesNotStart() { private FakeDownloadAction assertDoesNotStart() throws InterruptedException {
assertThat(downloader.started.block(ASSERT_FALSE_TIME)).isFalse(); Thread.sleep(ASSERT_FALSE_TIME);
assertThat(downloader.started.getCount()).isEqualTo(1);
return this; return this;
} }
private FakeDownloadAction assertStarted() { private FakeDownloadAction assertStarted() throws InterruptedException {
downloader.assertStarted(ASSERT_TRUE_TIMEOUT); downloader.assertStarted(ASSERT_TRUE_TIMEOUT);
return assertState(DownloadState.STATE_STARTED); return assertState(DownloadState.STATE_STARTED);
} }
...@@ -636,16 +659,18 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -636,16 +659,18 @@ public class DownloadManagerTest extends InstrumentationTestCase {
} }
private static class FakeDownloader implements Downloader { private static class FakeDownloader implements Downloader {
private final ConditionVariable started;
private final com.google.android.exoplayer2.util.ConditionVariable blocker; private final com.google.android.exoplayer2.util.ConditionVariable blocker;
private final boolean removeAction; private final boolean removeAction;
private CountDownLatch started;
private boolean ignoreInterrupts; private boolean ignoreInterrupts;
private volatile boolean enableDownloadIOException; private volatile boolean enableDownloadIOException;
private volatile int downloadedBytes = C.LENGTH_UNSET; private volatile int downloadedBytes = C.LENGTH_UNSET;
private FakeDownloader(boolean removeAction) { private FakeDownloader(boolean removeAction) {
this.removeAction = removeAction; this.removeAction = removeAction;
this.started = new ConditionVariable(); this.started = new CountDownLatch(1);
this.blocker = new com.google.android.exoplayer2.util.ConditionVariable(); this.blocker = new com.google.android.exoplayer2.util.ConditionVariable();
} }
...@@ -658,7 +683,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -658,7 +683,7 @@ public class DownloadManagerTest extends InstrumentationTestCase {
public void download(@Nullable ProgressListener listener) public void download(@Nullable ProgressListener listener)
throws InterruptedException, IOException { throws InterruptedException, IOException {
assertThat(removeAction).isFalse(); assertThat(removeAction).isFalse();
started.open(); started.countDown();
block(); block();
if (enableDownloadIOException) { if (enableDownloadIOException) {
throw new IOException(); throw new IOException();
...@@ -668,7 +693,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -668,7 +693,7 @@ public class DownloadManagerTest extends InstrumentationTestCase {
@Override @Override
public void remove() throws InterruptedException { public void remove() throws InterruptedException {
assertThat(removeAction).isTrue(); assertThat(removeAction).isTrue();
started.open(); started.countDown();
block(); block();
} }
...@@ -689,9 +714,9 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -689,9 +714,9 @@ public class DownloadManagerTest extends InstrumentationTestCase {
} }
} }
private FakeDownloader assertStarted(int timeout) { private FakeDownloader assertStarted(int timeout) throws InterruptedException {
assertThat(started.block(timeout)).isTrue(); assertThat(started.await(timeout, TimeUnit.MILLISECONDS)).isTrue();
started.close(); started = new CountDownLatch(1);
return this; return this;
} }
...@@ -710,5 +735,4 @@ public class DownloadManagerTest extends InstrumentationTestCase { ...@@ -710,5 +735,4 @@ public class DownloadManagerTest extends InstrumentationTestCase {
return Float.NaN; return Float.NaN;
} }
} }
} }
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 sign in to comment