diff --git a/app/build.gradle b/app/build.gradle index 95503ebbb..1d0cb3f56 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -98,6 +98,10 @@ android { buildFeatures { viewBinding = true } + dataBinding { + enabled = true + enabledForTests = true + } namespace 'org.openobservatory.ooniprobe' } @@ -114,6 +118,10 @@ dependencies { implementation 'com.google.guava:guava:30.1.1-android' implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' +// WorkManager dependency + implementation 'androidx.work:work-runtime:2.8.1' + // Third-party annotationProcessor 'com.github.Raizlabs.DBFlow:dbflow-processor:4.2.4' implementation 'com.github.Raizlabs.DBFlow:dbflow-core:4.2.4' diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/OoniRunActivity.java b/app/src/main/java/org/openobservatory/ooniprobe/activity/OoniRunActivity.java index df89ae0f7..306e5cba1 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/OoniRunActivity.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/OoniRunActivity.java @@ -96,7 +96,7 @@ private void manageIntent(Intent intent) { descriptorResponse -> { if (descriptorResponse!=null) { binding.progressIndicator.setVisibility(View.GONE); - loadScreen(descriptorResponse); + loadV2Screen(descriptorResponse); } else { binding.progressIndicator.setVisibility(View.GONE); loadInvalidAttributes(); @@ -135,8 +135,10 @@ else if (Intent.ACTION_SEND.equals(intent.getAction())) { } } - private void loadScreen(FetchTestDescriptorResponse response) { + private void loadV2Screen(FetchTestDescriptorResponse response) { + binding.v2Options.setVisibility(View.VISIBLE); + binding.items.setPadding(0, 0, 0, getResources().getDimensionPixelSize(R.dimen.activity_ooni_run_v2_items_margin_bottom)); binding.icon.setImageResource(response.suite.getIconGradient()); binding.icon.setColorFilter(getResources().getColor(R.color.color_gray7)); @@ -171,6 +173,8 @@ private void loadScreen(FetchTestDescriptorResponse response) { binding.run.setVisibility(View.VISIBLE); binding.run.setOnClickListener( v -> { + response.descriptor.setAutoUpdate(binding.autoUpdates.isChecked()); + response.descriptor.setAutoRun(binding.autoRun.isChecked()); response.descriptor.save(); ActivityCompat.startActivity(this, OverviewActivity.newIntent(this, response.suite), null); } diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/OverviewActivity.java b/app/src/main/java/org/openobservatory/ooniprobe/activity/OverviewActivity.java index 5d03d7e2b..c41ad5b40 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/OverviewActivity.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/OverviewActivity.java @@ -3,24 +3,35 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.text.format.DateUtils; +import android.util.Log; import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; import androidx.annotation.Nullable; import androidx.core.text.TextUtilsCompat; import androidx.core.view.ViewCompat; +import androidx.databinding.BindingAdapter; +import androidx.lifecycle.ViewModelProvider; + +import com.google.android.material.snackbar.BaseTransientBottomBar; +import com.google.android.material.snackbar.Snackbar; import org.openobservatory.ooniprobe.R; +import org.openobservatory.ooniprobe.activity.overview.OverviewViewModel; import org.openobservatory.ooniprobe.common.PreferenceManager; +import org.openobservatory.ooniprobe.common.TaskExecutor; +import org.openobservatory.ooniprobe.common.ThirdPartyServices; import org.openobservatory.ooniprobe.databinding.ActivityOverviewBinding; -import org.openobservatory.ooniprobe.model.database.Result; +import org.openobservatory.ooniprobe.domain.TestDescriptorManager; +import org.openobservatory.ooniprobe.model.database.TestDescriptor; import org.openobservatory.ooniprobe.test.suite.AbstractSuite; import org.openobservatory.ooniprobe.test.suite.ExperimentalSuite; import org.openobservatory.ooniprobe.test.suite.OONIRunSuite; -import org.openobservatory.ooniprobe.test.suite.WebsitesSuite; import org.openobservatory.ooniprobe.test.test.AbstractTest; import java.util.Locale; +import java.util.Objects; import javax.inject.Inject; @@ -28,12 +39,15 @@ public class OverviewActivity extends AbstractActivity { private static final String TEST = "test"; + private static final String TAG = OverviewActivity.class.getSimpleName(); private ActivityOverviewBinding binding; private AbstractSuite testSuite; @Inject PreferenceManager preferenceManager; + @Inject + OverviewViewModel viewModel; public static Intent newIntent(Context context, AbstractSuite testSuite) { return new Intent(context, OverviewActivity.class).putExtra(TEST, testSuite); @@ -48,44 +62,123 @@ public static Intent newIntent(Context context, AbstractSuite testSuite) { setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - setTitle(testSuite.getTitle()); - binding.icon.setImageResource(testSuite.getIcon()); - binding.customUrl.setVisibility(testSuite.getName().equals(WebsitesSuite.NAME) ? View.VISIBLE : View.GONE); + binding.setViewmodel(viewModel); + binding.setLifecycleOwner(this); + onTestSuiteChanged(); if(testSuite.isTestEmpty(preferenceManager)){ binding.run.setAlpha(0.5F); binding.run.setEnabled(false); } - if (testSuite.getName().equals(ExperimentalSuite.NAME)) { - Markwon.setMarkdown(binding.desc, testSuite.getDesc1()); - if (TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL) - binding.desc.setTextDirection(View.TEXT_DIRECTION_RTL); - } else { - Markwon.setMarkdown(binding.desc, testSuite.getDesc1()); - } if (testSuite.getName().equals(OONIRunSuite.NAME)) { - binding.author.setText(String.format("Author : %s",((OONIRunSuite)testSuite).getDescriptor().getAuthor())); - binding.author.setVisibility(View.VISIBLE); + binding.swipeRefresh.setOnRefreshListener(this::initiateRefresh); + } else { + binding.swipeRefresh.setEnabled(false); } - Result lastResult = Result.getLastResult(testSuite.getName()); - if (lastResult == null) - binding.lastTime.setText(R.string.Dashboard_Overview_LastRun_Never); - else - binding.lastTime.setText(DateUtils.getRelativeTimeSpanString(lastResult.start_time.getTime())); - setUpOnCLickListeners(); } + private void onTestSuiteChanged() { + setTitle(testSuite.getTitle()); + viewModel.onTestSuiteChanged(testSuite); + binding.executePendingBindings(); + } + + private void setUpOnCLickListeners() { binding.run.setOnClickListener(view -> onRunClick()); binding.customUrl.setOnClickListener(view -> customUrlClick()); } - @Override protected void onResume() { - super.onResume(); - testSuite.setTestList((AbstractTest[]) null); - testSuite.getTestList(preferenceManager); - binding.runtime.setText(getString(R.string.twoParam, getString(testSuite.getDataUsage()), getString(R.string.Dashboard_Card_Seconds, testSuite.getRuntime(preferenceManager).toString()))); + + private void initiateRefresh() { + Log.i(TAG, "initiateRefresh"); + TestDescriptor descriptorToUpdate = ((OONIRunSuite)testSuite).getDescriptor(); + + TaskExecutor executor = new TaskExecutor(); + executor.executeTask( + () -> TestDescriptorManager.fetchDescriptorFromRunId( + descriptorToUpdate.getRunId(), + OverviewActivity.this + ), + descriptor -> { + if (descriptorToUpdate.shouldUpdate(descriptor)){ + if (descriptorToUpdate.isAutoUpdate()) { + updateDescriptor(descriptor, descriptorToUpdate); + } else { + prepareForUpdates(descriptor, descriptorToUpdate); + } + } else { + noUpdatesAvailable(); + } + binding.swipeRefresh.setRefreshing(false); + return null; + }); + } + + private void updateDescriptor(TestDescriptor descriptor, TestDescriptor descriptorToUpdate) { + descriptor.setAutoUpdate(descriptorToUpdate.isAutoUpdate()); + descriptor.setAutoRun(descriptorToUpdate.isAutoRun()); + descriptor.save(); + binding.refresh.setVisibility(View.GONE); + updateViewFromDescriptor(descriptor); + Snackbar.make( + binding.getRoot(), + "Update Successful", + BaseTransientBottomBar.LENGTH_LONG + ).show(); + } + + private void prepareForUpdates(TestDescriptor descriptor, TestDescriptor descriptorToUpdate) { + binding.refresh.setOnClickListener(v -> updateDescriptor(descriptor, descriptorToUpdate)); + binding.refresh.setVisibility(android.view.View.VISIBLE); + } + + private void noUpdatesAvailable() { + Snackbar.make( + binding.getRoot(), + "No Updates available", + BaseTransientBottomBar.LENGTH_LONG + ).show(); + } + + private void updateViewFromDescriptor(TestDescriptor descriptor) { + this.testSuite = descriptor.getTestSuite(this); + this.onTestSuiteChanged(); + } + + @BindingAdapter(value = {"richText", "testSuiteName"}) + public static void setRichText(TextView view, String richText,String testSuiteName) { + try { + if (Objects.equals(testSuiteName,ExperimentalSuite.NAME)) { + Markwon.setMarkdown(view, richText); + if (TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL) + view.setTextDirection(View.TEXT_DIRECTION_RTL); + } else { + Markwon.setMarkdown(view, richText); + } + } catch (Exception e) { + e.printStackTrace(); + ThirdPartyServices.logException(e); + } + } + + @BindingAdapter({"resource"}) + public static void setImageViewResource(ImageView imageView, int resource) { + imageView.setImageResource(resource); + } + + @BindingAdapter({"dataUsage", "runTime"}) + public static void setDataUsage(TextView view, int dataUsage, String runTime) { + Context context = view.getContext(); + view.setText( + context.getString( + R.string.twoParam, + context.getString(dataUsage), + context.getString(R.string.Dashboard_Card_Seconds, runTime) + ) + ); + } void onRunClick() { diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/overview/OverviewViewModel.java b/app/src/main/java/org/openobservatory/ooniprobe/activity/overview/OverviewViewModel.java new file mode 100644 index 000000000..cfcc32395 --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/overview/OverviewViewModel.java @@ -0,0 +1,71 @@ +package org.openobservatory.ooniprobe.activity.overview; + +import android.text.format.DateUtils; +import android.view.View; + +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import org.jetbrains.annotations.NotNull; +import org.openobservatory.ooniprobe.R; +import org.openobservatory.ooniprobe.common.PreferenceManager; +import org.openobservatory.ooniprobe.common.ThirdPartyServices; +import org.openobservatory.ooniprobe.model.database.Result; +import org.openobservatory.ooniprobe.test.suite.AbstractSuite; +import org.openobservatory.ooniprobe.test.suite.OONIRunSuite; +import org.openobservatory.ooniprobe.test.suite.WebsitesSuite; +import org.openobservatory.ooniprobe.test.test.AbstractTest; + +import java.util.Objects; + +import javax.inject.Inject; + +public class OverviewViewModel extends ViewModel { + public MutableLiveData suite = new MutableLiveData<>(); + PreferenceManager preferenceManager; + + @Inject + public OverviewViewModel(@NotNull PreferenceManager preferenceManager) { + this.preferenceManager = preferenceManager; + } + + public void onTestSuiteChanged(AbstractSuite testSuite) { + suite.setValue(testSuite); + } + + public int getDataUsage() { + return (suite.getValue() != null) ? suite.getValue().getDataUsage() : R.string.TestResults_NotAvailable; + } + + public String getRunTime() { + try { + suite.getValue().setTestList((AbstractTest[]) null); + suite.getValue().getTestList(preferenceManager); + return suite.getValue().getRuntime(preferenceManager).toString(); + } catch (Exception e) { + ThirdPartyServices.logException(e); + return null; + } + } + + public String getName() { + return (suite.getValue() != null) ? suite.getValue().getName() : null; + } + + public String getLastTime() { + Result lastResult = Result.getLastResult(getName()); + return lastResult != null ? DateUtils.getRelativeTimeSpanString(lastResult.start_time.getTime()).toString() : null; + } + + public String getAuthor() { + return suite.getValue() instanceof OONIRunSuite ? String.format("Author : %s", ((OONIRunSuite) suite.getValue()).getDescriptor().getAuthor()) : null; + } + + public int getAuthorVisibility() { + return (getAuthor() != null) ? View.VISIBLE : View.GONE; + } + + public int getCustomUrlVisibility() { + return Objects.equals(getName(), WebsitesSuite.NAME) ? View.VISIBLE : View.GONE; + } +} diff --git a/app/src/main/java/org/openobservatory/ooniprobe/common/Application.java b/app/src/main/java/org/openobservatory/ooniprobe/common/Application.java index b00093dc0..7f1e444c4 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/common/Application.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/common/Application.java @@ -1,5 +1,7 @@ package org.openobservatory.ooniprobe.common; +import static androidx.work.WorkManager.getInstance; + import android.app.ActivityManager; import android.content.Context; import android.content.Intent; @@ -9,6 +11,12 @@ import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.OnLifecycleEvent; import androidx.lifecycle.ProcessLifecycleOwner; +import androidx.work.Constraints; +import androidx.work.ExistingPeriodicWorkPolicy; +import androidx.work.ExistingWorkPolicy; +import androidx.work.NetworkType; +import androidx.work.OneTimeWorkRequest; +import androidx.work.PeriodicWorkRequest; import com.google.gson.Gson; import com.raizlabs.android.dbflow.config.FlowLog; @@ -18,6 +26,7 @@ import org.openobservatory.ooniprobe.BuildConfig; import org.openobservatory.ooniprobe.client.OONIAPIClient; import org.openobservatory.ooniprobe.common.service.RunTestService; +import org.openobservatory.ooniprobe.common.worker.UpdateDescriptorsWorker; import org.openobservatory.ooniprobe.di.ActivityComponent; import org.openobservatory.ooniprobe.di.AppComponent; import org.openobservatory.ooniprobe.di.ApplicationModule; @@ -29,6 +38,7 @@ import java.util.Locale; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; @@ -66,6 +76,27 @@ public class Application extends android.app.Application { ThirdPartyServices.reloadConsents(Application.this); LocaleUtils.setLocale(new Locale(_preferenceManager.getSettingsLanguage())); LocaleUtils.updateConfig(this, getBaseContext().getResources().getConfiguration()); + scheduleWorkers(); + } + + private void scheduleWorkers() { + getInstance(this) + .enqueueUniquePeriodicWork( + UpdateDescriptorsWorker.UPDATED_DESCRIPTORS_WORK_NAME, + ExistingPeriodicWorkPolicy.KEEP, + new PeriodicWorkRequest.Builder(UpdateDescriptorsWorker.class, 24, TimeUnit.HOURS) + .setConstraints( + new Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + ).build() + ); + /*getInstance(this) + .beginUniqueWork( + UpdateDescriptorsWorker.UPDATED_DESCRIPTORS_WORK_NAME, + ExistingWorkPolicy.REPLACE, + OneTimeWorkRequest.from(UpdateDescriptorsWorker.class) + ).enqueue();*/ } protected AppComponent buildDagger() { diff --git a/app/src/main/java/org/openobservatory/ooniprobe/common/TaskExecutor.kt b/app/src/main/java/org/openobservatory/ooniprobe/common/TaskExecutor.kt index 4c9808382..37870057c 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/common/TaskExecutor.kt +++ b/app/src/main/java/org/openobservatory/ooniprobe/common/TaskExecutor.kt @@ -13,7 +13,7 @@ typealias Task = Callable typealias OnTaskProgressUpdate

= (P) -> Unit -typealias OnTaskComplete = (R) -> Void +typealias OnTaskComplete = (R) -> Unit class TaskExecutor { private val executor = Executors.newSingleThreadExecutor() diff --git a/app/src/main/java/org/openobservatory/ooniprobe/common/service/ServiceUtil.java b/app/src/main/java/org/openobservatory/ooniprobe/common/service/ServiceUtil.java index 1bcebd986..5a2e9a869 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/common/service/ServiceUtil.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/common/service/ServiceUtil.java @@ -18,6 +18,7 @@ import org.openobservatory.ooniprobe.common.PreferenceManager; import org.openobservatory.ooniprobe.common.ReachabilityManager; import org.openobservatory.ooniprobe.domain.GenerateAutoRunServiceSuite; +import org.openobservatory.ooniprobe.domain.TestDescriptorManager; import org.openobservatory.ooniprobe.test.suite.AbstractSuite; import org.openobservatory.ooniprobe.test.suite.CircumventionSuite; import org.openobservatory.ooniprobe.test.suite.ExperimentalSuite; @@ -87,6 +88,8 @@ public static void callCheckInAPI(Application app) { testSuites.add(CircumventionSuite.initForAutoRun(app.getResources())); testSuites.add(PerformanceSuite.initForAutoRun(app.getResources())); testSuites.add(ExperimentalSuite.initForAutoRun(app.getResources())); + + testSuites.addAll(TestDescriptorManager.descriptorsWithAutoRunEnabled(app)); ServiceUtil.startRunTestService(app, testSuites, false); } diff --git a/app/src/main/java/org/openobservatory/ooniprobe/common/worker/UpdateDescriptorsWorker.java b/app/src/main/java/org/openobservatory/ooniprobe/common/worker/UpdateDescriptorsWorker.java new file mode 100644 index 000000000..303ed3eb7 --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/common/worker/UpdateDescriptorsWorker.java @@ -0,0 +1,123 @@ +package org.openobservatory.ooniprobe.common.worker; + +import android.Manifest; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.work.Data; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import org.openobservatory.ooniprobe.R; +import org.openobservatory.ooniprobe.common.Application; +import org.openobservatory.ooniprobe.common.ThirdPartyServices; +import org.openobservatory.ooniprobe.domain.TestDescriptorManager; +import org.openobservatory.ooniprobe.model.database.TestDescriptor; + +import java.util.ArrayList; + +public class UpdateDescriptorsWorker extends Worker { + public static final String UPDATED_DESCRIPTORS_WORK_NAME = String.format("%s.UPDATED_DESCRIPTORS_WORK_NAME", UpdateDescriptorsWorker.class.getName()); + private static final String TAG = UpdateDescriptorsWorker.class.getSimpleName(); + public static final String UPDATE_DESCRIPTOR_CHANNEL = UpdateDescriptorsWorker.class.getSimpleName(); + private static final String KEY_UPDATED_DESCRIPTORS = String.format("%s.KEY_UPDATED_DESCRIPTORS", UpdateDescriptorsWorker.class.getName()); + + public UpdateDescriptorsWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @NonNull + @Override + public Result doWork() { + + try { + Context applicationContext = getApplicationContext(); + + Log.d(TAG, "Fetching descriptors from input"); + makeStatusNotification("Starting update for descriptors"); + + ArrayList updatedDescriptors = new ArrayList<>(); + + for (TestDescriptor descriptor : TestDescriptorManager.descriptorsWithAutoUpdateEnabled()) { + + Log.d(TAG, String.format("Fetching updates for %d ", descriptor.getRunId())); + makeStatusNotification(String.format("Fetching updates for %s ", descriptor.getName())); + + TestDescriptor updatedDescriptor = TestDescriptorManager.fetchDescriptorFromRunId(descriptor.getRunId(), applicationContext); + + if (descriptor.shouldUpdate(updatedDescriptor)) { + + updatedDescriptor.setAutoUpdate(descriptor.isAutoUpdate()); + updatedDescriptor.setAutoRun(descriptor.isAutoRun()); + + Log.d(TAG, String.format("Saving updates for %d ", descriptor.getRunId())); + makeStatusNotification(String.format("Saving updates for %s", descriptor.getName())); + + updatedDescriptor.save(); + updatedDescriptors.add(updatedDescriptor); + } + + } + + Data outputData = new Data.Builder().putString(KEY_UPDATED_DESCRIPTORS, ((Application) applicationContext).getGson().toJson(updatedDescriptors)).build(); + makeStatusNotification("Descriptor updates complete"); + return Result.success(outputData); + + } catch (Exception exception) { + Log.e(TAG, "Error Updating"); + makeStatusNotification("Error Updating"); + exception.printStackTrace(); + ThirdPartyServices.logException(exception); + return Result.failure(); + } + } + + /** + * Create a Notification that is shown as a heads-up notification if possible. + * + * @param message Message shown on the notification + */ + private void makeStatusNotification(String message) { + + // Make a channel if necessary + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + int importance = NotificationManager.IMPORTANCE_HIGH; + NotificationChannel channel = + new NotificationChannel(UPDATE_DESCRIPTOR_CHANNEL, "Run Descriptor Updates", importance); + channel.setDescription("Shows notification related to updates being made to Run Descriptors"); + + // Add the channel + NotificationManager notificationManager = + (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); + + if (notificationManager != null) { + notificationManager.createNotificationChannel(channel); + } + } + + // Create the notification + NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), UPDATE_DESCRIPTOR_CHANNEL) + .setSmallIcon(R.drawable.notification_icon) + .setContentTitle("Descriptor Update") + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setVibrate(new long[0]); + + // Show the notification + if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + return; + } + NotificationManagerCompat.from(getApplicationContext()).notify((int) System.currentTimeMillis(), builder.build()); + } +} + diff --git a/app/src/main/java/org/openobservatory/ooniprobe/domain/TestDescriptorManager.java b/app/src/main/java/org/openobservatory/ooniprobe/domain/TestDescriptorManager.java index 4447154c3..340a391d7 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/domain/TestDescriptorManager.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/domain/TestDescriptorManager.java @@ -13,11 +13,9 @@ import org.openobservatory.ooniprobe.BuildConfig; import org.openobservatory.ooniprobe.common.Application; import org.openobservatory.ooniprobe.model.database.TestDescriptor; -import org.openobservatory.ooniprobe.model.database.Url; +import org.openobservatory.ooniprobe.model.database.TestDescriptor_Table; import org.openobservatory.ooniprobe.test.EngineProvider; import org.openobservatory.ooniprobe.test.suite.OONIRunSuite; -import org.openobservatory.ooniprobe.test.test.AbstractTest; -import org.openobservatory.ooniprobe.test.test.WebConnectivity; import java.util.List; @@ -30,7 +28,7 @@ public static List getAll() { return SQLite.select().from(TestDescriptor.class).queryList(); } - public static FetchTestDescriptorResponse fetchDataFromRunId(long runId, Context context) throws Exception{ + public static TestDescriptor fetchDescriptorFromRunId(long runId, Context context) throws Exception{ OONISession session = EngineProvider.get().newSession( EngineProvider.get().getDefaultSessionConfig( context, @@ -45,19 +43,7 @@ public static FetchTestDescriptorResponse fetchDataFromRunId(long runId, Context OONIRunFetchResponse response = session.ooniRunFetch(ooniContext, runId); OONIRunDescriptor descriptor = response.descriptor; - List tests = Lists.transform( - descriptor.getNettests(), - nettest -> { - AbstractTest test = AbstractTest.getTestByName(nettest.getName()); - if (nettest.getName().equals(WebConnectivity.NAME)){ - for (String url : nettest.getInputs()) - Url.checkExistingUrl(url); - } - test.setInputs(nettest.getInputs()); - return test; - } - ); - TestDescriptor testDescriptor = TestDescriptor.Builder.aTestDescriptor() + return TestDescriptor.Builder.aTestDescriptor() .withRunId(runId) .withName(descriptor.getName()) .withNameIntl(descriptor.getNameIntl()) @@ -66,22 +52,37 @@ public static FetchTestDescriptorResponse fetchDataFromRunId(long runId, Context .withDescription(descriptor.getDescription()) .withDescriptionIntl(descriptor.getDescriptionIntl()) .withIcon(descriptor.getIcon()) - .withArchived(descriptor.getArchived()) + .withArchived(response.archived) .withAuthor(descriptor.getAuthor()) + .withCreationTime(response.creationTime) + .withTranslationCreationTime(response.translationCreationTime) .withNettests(descriptor.getNettests()) .build(); + } + + public static FetchTestDescriptorResponse fetchDataFromRunId(long runId, Context context) throws Exception{ + TestDescriptor testDescriptor = fetchDescriptorFromRunId(runId,context); return new FetchTestDescriptorResponse( - new OONIRunSuite( - context, - testDescriptor, - tests.toArray(new AbstractTest[0]) - ), + testDescriptor.getTestSuite(context), testDescriptor ); } + public static List descriptorsWithAutoRunEnabled(Context context){ + List descriptors = TestDescriptor.selectAllAvailable() + .and(TestDescriptor_Table.auto_run.eq(true)) + .queryList(); + return Lists.transform(descriptors, input -> input.getTestSuite(context)); + } + + public static List descriptorsWithAutoUpdateEnabled(){ + return TestDescriptor.selectAllAvailable() + .and(TestDescriptor_Table.auto_update.eq(true)) + .queryList(); + } + public static class FetchTestDescriptorResponse { public OONIRunSuite suite; public TestDescriptor descriptor; diff --git a/app/src/main/java/org/openobservatory/ooniprobe/model/database/TestDescriptor.java b/app/src/main/java/org/openobservatory/ooniprobe/model/database/TestDescriptor.java index de97e9f51..2ee2a0203 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/model/database/TestDescriptor.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/model/database/TestDescriptor.java @@ -1,17 +1,27 @@ package org.openobservatory.ooniprobe.model.database; +import android.content.Context; + +import com.google.common.collect.Lists; import com.raizlabs.android.dbflow.annotation.Column; import com.raizlabs.android.dbflow.annotation.PrimaryKey; import com.raizlabs.android.dbflow.annotation.Table; +import com.raizlabs.android.dbflow.sql.language.SQLite; +import com.raizlabs.android.dbflow.sql.language.Where; import com.raizlabs.android.dbflow.structure.BaseModel; +import org.openobservatory.engine.OONIRunNettest; import org.openobservatory.ooniprobe.common.AppDatabase; import org.openobservatory.ooniprobe.common.LocaleUtils; import org.openobservatory.ooniprobe.common.MapUtility; import org.openobservatory.ooniprobe.domain.MapConverter; import org.openobservatory.ooniprobe.domain.NettestConverter; +import org.openobservatory.ooniprobe.test.suite.OONIRunSuite; +import org.openobservatory.ooniprobe.test.test.AbstractTest; +import org.openobservatory.ooniprobe.test.test.WebConnectivity; import java.io.Serializable; +import java.util.Date; import java.util.HashMap; import java.util.List; @@ -47,6 +57,17 @@ public class TestDescriptor extends BaseModel implements Serializable { @Column private boolean archived; + @Column(name = "auto_run") + private boolean autoRun; + + @Column(name = "auto_update") + private boolean autoUpdate; + + @Column(name = "creation_time") + private Date creationTime; + + @Column(name = "translation_creation_time") + private Date translationCreationTime; @Column(typeConverter = NettestConverter.class) private List nettests; @@ -130,6 +151,38 @@ public void setArchived(boolean archived) { this.archived = archived; } + public boolean isAutoRun() { + return autoRun; + } + + public void setAutoRun(boolean autoRun) { + this.autoRun = autoRun; + } + + public boolean isAutoUpdate() { + return autoUpdate; + } + + public void setAutoUpdate(boolean autoUpdate) { + this.autoUpdate = autoUpdate; + } + + public Date getCreationTime() { + return creationTime; + } + + public void setCreationTime(Date creationTime) { + this.creationTime = creationTime; + } + + public Date getTranslationCreationTime() { + return translationCreationTime; + } + + public void setTranslationCreationTime(Date translationCreationTime) { + this.translationCreationTime = translationCreationTime; + } + public List getNettests() { return nettests; } @@ -138,6 +191,36 @@ public void setNettests(List nettests) { this.nettests = nettests; } + public OONIRunSuite getTestSuite(Context context) { + List tests = Lists.transform( + (List)getNettests(), + nettest -> { + AbstractTest test = AbstractTest.getTestByName(nettest.getName()); + if (nettest.getName().equals(WebConnectivity.NAME)){ + for (String url : nettest.getInputs()) + Url.checkExistingUrl(url); + } + test.setInputs(nettest.getInputs()); + return test; + } + ); + return new OONIRunSuite( + context, + this, + tests.toArray(new AbstractTest[0]) + ); + } + + public static Where selectAllAvailable() { + return SQLite.select().from(TestDescriptor.class) + .where(TestDescriptor_Table.archived.eq(false)); + } + + public boolean shouldUpdate(TestDescriptor updatedDescriptor) { + return updatedDescriptor.creationTime.after(creationTime) + || updatedDescriptor.translationCreationTime.after(translationCreationTime); + } + public static final class Builder { private long runId; @@ -150,6 +233,10 @@ public static final class Builder { private String icon; private String author; private boolean archived; + private boolean autoRun; + private boolean autoUpdate; + private Date creationTime; + private Date translationCreationTime; private List nettests; private Builder() { @@ -209,6 +296,26 @@ public Builder withArchived(boolean archived) { return this; } + public Builder withAutoRun(boolean autoRun) { + this.autoRun = autoRun; + return this; + } + + public Builder withAutoUpdate(boolean autoUpdate) { + this.autoUpdate = autoUpdate; + return this; + } + + public Builder withCreationTime(Date creationTime) { + this.creationTime = creationTime; + return this; + } + + public Builder withTranslationCreationTime(Date translationCreationTime) { + this.translationCreationTime = translationCreationTime; + return this; + } + public Builder withNettests(List nettests) { this.nettests = nettests; return this; @@ -226,6 +333,10 @@ public TestDescriptor build() { testDescriptor.setIcon(icon); testDescriptor.setAuthor(author); testDescriptor.setArchived(archived); + testDescriptor.setAutoRun(autoRun); + testDescriptor.setAutoUpdate(autoUpdate); + testDescriptor.setCreationTime(creationTime); + testDescriptor.setTranslationCreationTime(translationCreationTime); testDescriptor.setNettests(nettests); return testDescriptor; } diff --git a/app/src/main/java/org/openobservatory/ooniprobe/test/TestAsyncTask.java b/app/src/main/java/org/openobservatory/ooniprobe/test/TestAsyncTask.java index ddf409ff4..23baf0b33 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/test/TestAsyncTask.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/test/TestAsyncTask.java @@ -76,13 +76,10 @@ public static List getSuites(Context context) { List testDescriptors = TestDescriptorManager.getAll(); testSuites.addAll( - Lists.transform(testDescriptors,descriptor -> { - List tests = Lists.transform( - (List)descriptor.getNettests(), - nettest -> AbstractTest.getTestByName(nettest.getName()) - ); - return new OONIRunSuite(context, descriptor, tests.toArray(new AbstractTest[0])); - }) + Lists.transform( + testDescriptors, + descriptor -> descriptor.getTestSuite(context) + ) ); return testSuites; } diff --git a/app/src/main/res/layout/activity_oonirun.xml b/app/src/main/res/layout/activity_oonirun.xml index 911d45cb4..dfb5fd08a 100644 --- a/app/src/main/res/layout/activity_oonirun.xml +++ b/app/src/main/res/layout/activity_oonirun.xml @@ -3,7 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + tools:context=".activity.OoniRunActivity"> -