Commit b10fbe55 authored by fgorski's avatar fgorski Committed by Commit bot

[Android] Implements OS upgrade check and rescheduling

* Adds an OS upgrade check to the BackgroundTaskScheduler
* Adds OS version tracking to BackgroundTaskSchedulerPrefs
* Ensures GCMNetworkManager based upgrades only happen on supported OSs
* Calls OS version check from DeferredStartupHandler

BUG=710630
R=nyquist@chromium.org

Review-Url: https://codereview.chromium.org/2819703002
Cr-Commit-Position: refs/heads/master@{#473957}
parent 01a6ade0
......@@ -4,6 +4,8 @@
package org.chromium.components.background_task_scheduler;
import android.os.Build;
import com.google.android.gms.gcm.GcmNetworkManager;
import com.google.android.gms.gcm.GcmTaskService;
import com.google.android.gms.gcm.TaskParams;
......@@ -125,6 +127,8 @@ public class BackgroundTaskGcmTaskService extends GcmTaskService {
@Override
public void onInitializeTasks() {
// Ignore the event on OSs supporting JobScheduler.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) return;
BackgroundTaskSchedulerFactory.getScheduler().reschedule(
ContextUtils.getApplicationContext());
}
......
......@@ -95,6 +95,41 @@ public class BackgroundTaskScheduler {
mSchedulerDelegate.cancel(context, taskId);
}
/**
* Checks whether OS was upgraded and triggers rescheduling if it is necessary.
* Rescheduling is necessary if type of background task scheduler delegate is different for a
* new version of the OS.
*
* @param context the current context.
*/
public void checkForOSUpgrade(Context context) {
int oldSdkInt = BackgroundTaskSchedulerPrefs.getLastSdkVersion();
int newSdkInt = Build.VERSION.SDK_INT;
// No OS upgrade detected.
if (oldSdkInt == newSdkInt) return;
// Save the current SDK version to preferences.
BackgroundTaskSchedulerPrefs.setLastSdkVersion(newSdkInt);
// Check for OS upgrades forcing delegate change or "just in case" rescheduling.
if (!osUpgradeChangesDelegateType(oldSdkInt, newSdkInt)) return;
// Explicitly create and invoke old delegate type to cancel all scheduled tasks.
// All preference entries are kept until reschedule call, which removes then then.
BackgroundTaskSchedulerDelegate oldDelegate =
BackgroundTaskSchedulerFactory.getSchedulerDelegateForSdk(oldSdkInt);
Set<Integer> scheduledTaskIds = BackgroundTaskSchedulerPrefs.getScheduledTaskIds();
for (int taskId : scheduledTaskIds) {
oldDelegate.cancel(context, taskId);
}
reschedule(context);
}
/**
* Reschedules all the tasks currently scheduler through BackgroundTaskSheduler.
* @param context the current context.
*/
public void reschedule(Context context) {
Set<String> scheduledTasksClassNames = BackgroundTaskSchedulerPrefs.getScheduledTasks();
BackgroundTaskSchedulerPrefs.removeAllTasks();
......@@ -108,4 +143,8 @@ public class BackgroundTaskScheduler {
task.reschedule(context);
}
}
private boolean osUpgradeChangesDelegateType(int oldSdkInt, int newSdkInt) {
return oldSdkInt < Build.VERSION_CODES.M && newSdkInt >= Build.VERSION_CODES.M;
}
}
......@@ -4,7 +4,6 @@
package org.chromium.components.background_task_scheduler;
import android.annotation.TargetApi;
import android.os.Build;
import org.chromium.base.ThreadUtils;
......@@ -16,9 +15,8 @@ import org.chromium.base.VisibleForTesting;
public final class BackgroundTaskSchedulerFactory {
private static BackgroundTaskScheduler sInstance;
@TargetApi(Build.VERSION_CODES.M)
private static BackgroundTaskSchedulerDelegate getSchedulerDelegate() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
static BackgroundTaskSchedulerDelegate getSchedulerDelegateForSdk(int sdkInt) {
if (sdkInt >= Build.VERSION_CODES.M) {
return new BackgroundTaskSchedulerJobService();
} else {
return new BackgroundTaskSchedulerGcmNetworkManager();
......@@ -31,7 +29,10 @@ public final class BackgroundTaskSchedulerFactory {
*/
public static BackgroundTaskScheduler getScheduler() {
ThreadUtils.assertOnUiThread();
if (sInstance == null) sInstance = new BackgroundTaskScheduler(getSchedulerDelegate());
if (sInstance == null) {
sInstance =
new BackgroundTaskScheduler(getSchedulerDelegateForSdk(Build.VERSION.SDK_INT));
}
return sInstance;
}
......
......@@ -5,6 +5,7 @@
package org.chromium.components.background_task_scheduler;
import android.content.SharedPreferences;
import android.os.Build;
import org.chromium.base.ContextUtils;
......@@ -17,16 +18,75 @@ import java.util.Set;
public class BackgroundTaskSchedulerPrefs {
private static final String TAG = "BTSPrefs";
private static final String KEY_SCHEDULED_TASKS = "bts_scheduled_tasks";
private static final String KEY_LAST_OS_VERSION = "bts_last_os_version";
private static final String KEY_LAST_APP_VERSION = "bts_last_app_version";
private static final String ENTRY_SEPARATOR = ":";
private static final String KEY_LAST_SDK_VERSION = "bts_last_sdk_version";
/**
* Class abstracting conversions between a string kept in shared preferences and actual values
* held there.
*/
private static class ScheduledTaskPreferenceEntry {
private static final String ENTRY_SEPARATOR = ":";
private String mBackgroundTaskClass;
private int mTaskId;
/** Creates a scheduled task shared preference entry from task info. */
public static ScheduledTaskPreferenceEntry createForTaskInfo(TaskInfo taskInfo) {
return new ScheduledTaskPreferenceEntry(
taskInfo.getBackgroundTaskClass().getName(), taskInfo.getTaskId());
}
/**
* Parses a preference entry from input string.
*
* @param entry An input string to parse.
* @return A parsed entry or null if the input is not valid.
*/
public static ScheduledTaskPreferenceEntry parseEntry(String entry) {
if (entry == null) return null;
String[] entryParts = entry.split(ENTRY_SEPARATOR);
if (entryParts.length != 2 || entryParts[0].isEmpty() || entryParts[1].isEmpty()) {
return null;
}
int taskId = 0;
try {
taskId = Integer.parseInt(entryParts[1]);
} catch (NumberFormatException e) {
return null;
}
return new ScheduledTaskPreferenceEntry(entryParts[0], taskId);
}
public ScheduledTaskPreferenceEntry(String className, int taskId) {
mBackgroundTaskClass = className;
mTaskId = taskId;
}
/**
* Converts a task info to a shared preference entry in the format:
* BACKGROUND_TASK_CLASS_NAME:TASK_ID.
*/
public String toString() {
return mBackgroundTaskClass + ENTRY_SEPARATOR + mTaskId;
}
/** Gets the name of background task class in this entry. */
public String getBackgroundTaskClass() {
return mBackgroundTaskClass;
}
/** Gets the ID of the task in this entry. */
public int getTaskId() {
return mTaskId;
}
}
/** Adds a task to scheduler's preferences, so that it can be rescheduled with OS upgrade. */
public static void addScheduledTask(TaskInfo taskInfo) {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
Set<String> scheduledTasks =
prefs.getStringSet(KEY_SCHEDULED_TASKS, new HashSet<String>(1));
String prefsEntry = toSharedPreferenceEntry(taskInfo);
String prefsEntry = ScheduledTaskPreferenceEntry.createForTaskInfo(taskInfo).toString();
if (scheduledTasks.contains(prefsEntry)) return;
// Set returned from getStringSet() should not be modified.
......@@ -38,12 +98,12 @@ public class BackgroundTaskSchedulerPrefs {
/** Removes a task from scheduler's preferences. */
public static void removeScheduledTask(int taskId) {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
Set<String> scheduledTasks = prefs.getStringSet(KEY_SCHEDULED_TASKS, new HashSet<String>());
Set<String> scheduledTasks = getScheduledTaskEntries(prefs);
String entryToRemove = null;
String taskSuffix = ENTRY_SEPARATOR + taskId;
for (String entry : scheduledTasks) {
if (entry.endsWith(taskSuffix)) {
ScheduledTaskPreferenceEntry parsed = ScheduledTaskPreferenceEntry.parseEntry(entry);
if (parsed != null && parsed.getTaskId() == taskId) {
entryToRemove = entry;
break;
}
......@@ -61,36 +121,59 @@ public class BackgroundTaskSchedulerPrefs {
/** Gets a set of scheduled task class names. */
public static Set<String> getScheduledTasks() {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
Set<String> prefsEntries = prefs.getStringSet(KEY_SCHEDULED_TASKS, new HashSet<String>(1));
Set<String> scheduledTasksClassNames = new HashSet<>(prefsEntries.size());
for (String entry : prefsEntries) {
String[] entryParts = entry.split(ENTRY_SEPARATOR);
if (entryParts.length != 2 || entryParts[0] == null || entryParts[0].isEmpty()) {
continue;
Set<String> scheduledTask = getScheduledTaskEntries(prefs);
Set<String> scheduledTasksClassNames = new HashSet<>(scheduledTask.size());
for (String entry : scheduledTask) {
ScheduledTaskPreferenceEntry parsed = ScheduledTaskPreferenceEntry.parseEntry(entry);
if (parsed != null) {
scheduledTasksClassNames.add(parsed.getBackgroundTaskClass());
}
scheduledTasksClassNames.add(entryParts[0]);
}
return scheduledTasksClassNames;
}
/** Gets a set of scheduled task IDs. */
public static Set<Integer> getScheduledTaskIds() {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
Set<String> scheduledTasks = getScheduledTaskEntries(prefs);
Set<Integer> scheduledTaskIds = new HashSet<>(scheduledTasks.size());
for (String entry : scheduledTasks) {
ScheduledTaskPreferenceEntry parsed = ScheduledTaskPreferenceEntry.parseEntry(entry);
if (parsed != null) {
scheduledTaskIds.add(parsed.getTaskId());
}
}
return scheduledTaskIds;
}
/** Removes all scheduled tasks from shared preferences store. */
public static void removeAllTasks() {
ContextUtils.getAppSharedPreferences().edit().remove(KEY_SCHEDULED_TASKS).apply();
}
/** Gets the last SDK version on which this instance ran. Defaults to current SDK version. */
public static int getLastSdkVersion() {
return ContextUtils.getAppSharedPreferences().getInt(
KEY_LAST_SDK_VERSION, Build.VERSION.SDK_INT);
}
/** Gets the last SDK version on which this instance ran. */
public static void setLastSdkVersion(int sdkVersion) {
ContextUtils.getAppSharedPreferences()
.edit()
.putInt(KEY_LAST_SDK_VERSION, sdkVersion)
.apply();
}
private static void updateScheduledTasks(SharedPreferences prefs, Set<String> tasks) {
SharedPreferences.Editor editor = prefs.edit();
editor.putStringSet(KEY_SCHEDULED_TASKS, tasks);
editor.apply();
}
/**
* Converts a task info to a shared preference entry in the format:
* BACKGROUND_TASK_CLASS_NAME:TASK_ID.
* Task ID is necessary to be able to remove the entries from the shared preferences.
*/
private static String toSharedPreferenceEntry(TaskInfo taskInfo) {
return taskInfo.getBackgroundTaskClass().getName() + ENTRY_SEPARATOR + taskInfo.getTaskId();
/** Gets the entries for scheduled tasks from shared preferences. */
private static Set<String> getScheduledTaskEntries(SharedPreferences prefs) {
return prefs.getStringSet(KEY_SCHEDULED_TASKS, new HashSet<String>(1));
}
}
......@@ -8,6 +8,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import com.google.android.gms.gcm.GcmNetworkManager;
......@@ -20,6 +21,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
import org.chromium.base.ContextUtils;
import org.chromium.base.test.util.Feature;
......@@ -121,18 +123,33 @@ public class BackgroundTaskGcmTaskServiceTest {
@Test
@Feature({"BackgroundTaskScheduler"})
public void testOnInitializeTasks() {
public void testOnInitializeTasksOnPreM() {
ReflectionHelpers.setStaticField(
Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.LOLLIPOP);
TaskInfo task = TaskInfo.createOneOffTask(TaskIds.TEST, TestBackgroundTask.class,
TimeUnit.DAYS.toMillis(1))
.build();
BackgroundTaskSchedulerPrefs.addScheduledTask(task);
assertEquals(0, TestBackgroundTask.getRescheduleCalls());
BackgroundTaskGcmTaskService taskService = new BackgroundTaskGcmTaskService();
taskService.onInitializeTasks();
new BackgroundTaskGcmTaskService().onInitializeTasks();
assertEquals(1, TestBackgroundTask.getRescheduleCalls());
}
@Test
@Feature({"BackgroundTaskScheduler"})
public void testOnInitializeTasksOnMPlus() {
ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.M);
TaskInfo task = TaskInfo.createOneOffTask(TaskIds.TEST, TestBackgroundTask.class,
TimeUnit.DAYS.toMillis(1))
.build();
BackgroundTaskSchedulerPrefs.addScheduledTask(task);
assertEquals(0, TestBackgroundTask.getRescheduleCalls());
new BackgroundTaskGcmTaskService().onInitializeTasks();
assertEquals(0, TestBackgroundTask.getRescheduleCalls());
}
private TaskParams buildTaskParams(Class clazz, Bundle taskExtras) {
Bundle extras = new Bundle();
extras.putBundle(
......
......@@ -8,16 +8,20 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.os.Build;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
import org.chromium.base.ContextUtils;
import org.chromium.base.test.util.Feature;
import org.chromium.testing.local.LocalRobolectricTestRunner;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
......@@ -75,6 +79,11 @@ public class BackgroundTaskSchedulerPrefsTest {
scheduledTasks.contains(TASK_2.getBackgroundTaskClass().getName()));
assertTrue("task3 class name in scheduled tasks.",
scheduledTasks.contains(task3.getBackgroundTaskClass().getName()));
Set<Integer> taskIds = BackgroundTaskSchedulerPrefs.getScheduledTaskIds();
assertTrue(taskIds.contains(TASK_1.getTaskId()));
assertTrue(taskIds.contains(TASK_2.getTaskId()));
assertTrue(taskIds.contains(task3.getTaskId()));
}
@Test
......@@ -91,10 +100,33 @@ public class BackgroundTaskSchedulerPrefsTest {
BackgroundTaskSchedulerPrefs.getScheduledTasks().size());
Set<String> scheduledTasks = BackgroundTaskSchedulerPrefs.getScheduledTasks();
assertFalse("TASK_1 class name in scheduled tasks.",
assertFalse("TASK_1 class name is not in scheduled tasks.",
scheduledTasks.contains(TASK_1.getBackgroundTaskClass().getName()));
assertTrue("TASK_2 class name in scheduled tasks.",
scheduledTasks.contains(TASK_2.getBackgroundTaskClass().getName()));
Set<Integer> taskIds = BackgroundTaskSchedulerPrefs.getScheduledTaskIds();
assertFalse(taskIds.contains(TASK_1.getTaskId()));
assertTrue(taskIds.contains(TASK_2.getTaskId()));
}
@Test
@Feature("BackgroundTaskScheduler")
public void testUnparseableEntries() {
HashSet<String> badEntries = new HashSet<>();
badEntries.add(":123");
badEntries.add("Class:");
badEntries.add("Class:NotAnInt");
badEntries.add("Int field missing");
badEntries.add("Class:123:Too many fields");
badEntries.add("");
badEntries.add(null);
ContextUtils.getAppSharedPreferences()
.edit()
.putStringSet("bts_scheduled_tasks", badEntries)
.apply();
assertTrue(BackgroundTaskSchedulerPrefs.getScheduledTaskIds().isEmpty());
assertTrue(BackgroundTaskSchedulerPrefs.getScheduledTasks().isEmpty());
}
@Test
......@@ -105,5 +137,19 @@ public class BackgroundTaskSchedulerPrefsTest {
BackgroundTaskSchedulerPrefs.removeAllTasks();
assertTrue("We are expecting a all tasks to be gone.",
BackgroundTaskSchedulerPrefs.getScheduledTasks().isEmpty());
assertTrue("We are expecting a all tasks to be gone.",
BackgroundTaskSchedulerPrefs.getScheduledTaskIds().isEmpty());
}
@Test
@Feature("BackgroundTaskScheduler")
public void testLastSdkVersion() {
ReflectionHelpers.setStaticField(
Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.KITKAT);
assertEquals("Current SDK version should be default.", Build.VERSION_CODES.KITKAT,
BackgroundTaskSchedulerPrefs.getLastSdkVersion());
BackgroundTaskSchedulerPrefs.setLastSdkVersion(Build.VERSION_CODES.LOLLIPOP);
assertEquals(
Build.VERSION_CODES.LOLLIPOP, BackgroundTaskSchedulerPrefs.getLastSdkVersion());
}
}
......@@ -4,6 +4,12 @@
package org.chromium.components.background_task_scheduler;
import android.os.Build;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.gcm.GcmNetworkManager;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
......@@ -20,6 +26,10 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.internal.ShadowExtractor;
import org.robolectric.shadows.gms.Shadows;
import org.robolectric.shadows.gms.common.ShadowGoogleApiAvailability;
import org.robolectric.util.ReflectionHelpers;
import org.chromium.base.ContextUtils;
import org.chromium.base.test.util.Feature;
......@@ -29,7 +39,8 @@ import java.util.concurrent.TimeUnit;
/** Unit tests for {@link BackgroundTaskScheduler}. */
@RunWith(LocalRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@Config(manifest = Config.NONE,
shadows = {ShadowGcmNetworkManager.class, ShadowGoogleApiAvailability.class})
public class BackgroundTaskSchedulerTest {
private static final TaskInfo TASK =
TaskInfo.createOneOffTask(
......@@ -38,6 +49,7 @@ public class BackgroundTaskSchedulerTest {
@Mock
private BackgroundTaskSchedulerDelegate mDelegate;
private ShadowGcmNetworkManager mGcmNetworkManager;
@Before
public void setUp() {
......@@ -46,6 +58,12 @@ public class BackgroundTaskSchedulerTest {
BackgroundTaskSchedulerFactory.setSchedulerForTesting(
new BackgroundTaskScheduler(mDelegate));
TestBackgroundTask.reset();
// Initialize Google Play Services and GCM Network Manager for upgrade testing.
Shadows.shadowOf(GoogleApiAvailability.getInstance())
.setIsGooglePlayServicesAvailable(ConnectionResult.SUCCESS);
mGcmNetworkManager = (ShadowGcmNetworkManager) ShadowExtractor.extract(
GcmNetworkManager.getInstance(ContextUtils.getApplicationContext()));
}
@Test
......@@ -95,4 +113,52 @@ public class BackgroundTaskSchedulerTest {
assertEquals(1, TestBackgroundTask.getRescheduleCalls());
assertTrue(BackgroundTaskSchedulerPrefs.getScheduledTasks().isEmpty());
}
@Test
@Feature({"BackgroundTaskScheduler"})
public void testCheckForOSUpgrade_PreMToMPlus() {
BackgroundTaskSchedulerPrefs.setLastSdkVersion(Build.VERSION_CODES.LOLLIPOP);
BackgroundTaskSchedulerPrefs.addScheduledTask(TASK);
ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.M);
BackgroundTaskSchedulerFactory.getScheduler().checkForOSUpgrade(
RuntimeEnvironment.application);
assertEquals(Build.VERSION_CODES.M, BackgroundTaskSchedulerPrefs.getLastSdkVersion());
assertTrue(mGcmNetworkManager.getCanceledTaskTags().contains(
Integer.toString(TASK.getTaskId())));
assertEquals(1, TestBackgroundTask.getRescheduleCalls());
}
/** This scenario tests upgrade from pre-M to pre-M OS, which requires no rescheduling. */
@Test
@Feature({"BackgroundTaskScheduler"})
public void testCheckForOSUpgrade_PreMToPreM() {
BackgroundTaskSchedulerPrefs.setLastSdkVersion(Build.VERSION_CODES.KITKAT);
BackgroundTaskSchedulerPrefs.addScheduledTask(TASK);
ReflectionHelpers.setStaticField(
Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.LOLLIPOP);
BackgroundTaskSchedulerFactory.getScheduler().checkForOSUpgrade(
RuntimeEnvironment.application);
assertEquals(
Build.VERSION_CODES.LOLLIPOP, BackgroundTaskSchedulerPrefs.getLastSdkVersion());
assertEquals(0, TestBackgroundTask.getRescheduleCalls());
}
/** This scenario tests upgrade from M+ to M+ OS, which requires no rescheduling. */
@Test
@Feature({"BackgroundTaskScheduler"})
public void testCheckForOSUpgrade_MPlusToMPlus() {
BackgroundTaskSchedulerPrefs.setLastSdkVersion(Build.VERSION_CODES.M);
BackgroundTaskSchedulerPrefs.addScheduledTask(TASK);
ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.N);
BackgroundTaskSchedulerFactory.getScheduler().checkForOSUpgrade(
RuntimeEnvironment.application);
assertEquals(Build.VERSION_CODES.N, BackgroundTaskSchedulerPrefs.getLastSdkVersion());
assertEquals(0, TestBackgroundTask.getRescheduleCalls());
}
}
......@@ -11,6 +11,9 @@ import com.google.android.gms.gcm.Task;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import java.util.HashSet;
import java.util.Set;
/**
* Custom shadow for the OS's GcmNetworkManager. We use this to hook the call to GcmNetworkManager
* to make sure it was invoked as we expect.
......@@ -19,6 +22,11 @@ import org.robolectric.annotation.Implements;
public class ShadowGcmNetworkManager {
private Task mTask;
private Task mCanceledTask;
private Set<String> mCanceledTaskTags;
public ShadowGcmNetworkManager() {
mCanceledTaskTags = new HashSet<>();
}
@Implementation
public void schedule(Task task) {
......@@ -33,6 +41,7 @@ public class ShadowGcmNetworkManager {
mCanceledTask = mTask;
mTask = null;
}
mCanceledTaskTags.add(tag);
}
public Task getScheduledTask() {
......@@ -43,8 +52,13 @@ public class ShadowGcmNetworkManager {
return mCanceledTask;
}
public Set<String> getCanceledTaskTags() {
return mCanceledTaskTags;
}
public void clear() {
mTask = null;
mCanceledTask = null;
mCanceledTaskTags = new HashSet<>();
}
}
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