From 66ffbc30a11a357616bd61d909aaed57451f3b6d Mon Sep 17 00:00:00 2001 From: Norbel Ambanumben Date: Tue, 24 Oct 2023 21:50:49 +0100 Subject: [PATCH 01/12] Upgrade gradle to `8.1.2` and add support for kotlin and `kapt` --- .github/workflows/archive.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/emulator.yml | 2 +- .github/workflows/test.yml | 2 +- app/build.gradle | 18 +++++++++++------- app/jacoco.gradle | 20 +++++++++++++------- gradle.properties | 3 +++ gradle/libs.versions.toml | 8 ++++---- gradle/wrapper/gradle-wrapper.properties | 2 +- shared-test/build.gradle | 21 +++++++++++++-------- 10 files changed, 49 insertions(+), 31 deletions(-) diff --git a/.github/workflows/archive.yml b/.github/workflows/archive.yml index d41537923..40344f610 100644 --- a/.github/workflows/archive.yml +++ b/.github/workflows/archive.yml @@ -7,7 +7,7 @@ jobs: steps: - uses: actions/setup-java@v2 with: - java-version: '11' + java-version: '17' distribution: 'temurin' - name: checkout uses: actions/checkout@v2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6ed6c7a7e..46f49645d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/setup-java@v2 with: - java-version: '11' + java-version: '17' distribution: 'temurin' - name: checkout uses: actions/checkout@v2 diff --git a/.github/workflows/emulator.yml b/.github/workflows/emulator.yml index 0a599c15a..de92c9393 100644 --- a/.github/workflows/emulator.yml +++ b/.github/workflows/emulator.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/setup-java@v2 with: - java-version: '11' + java-version: '17' distribution: 'temurin' - name: checkout uses: actions/checkout@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e678fedd6..f743fca58 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: steps: - uses: actions/setup-java@v2 with: - java-version: '11' + java-version: '17' distribution: 'temurin' - name: checkout uses: actions/checkout@v2 diff --git a/app/build.gradle b/app/build.gradle index 5abfe08e1..1c215b084 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.application' - id 'org.jetbrains.kotlin.android' + id 'kotlin-android' + id 'kotlin-kapt' } apply from: 'jacoco.gradle' @@ -85,8 +86,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 } buildFeatures { viewBinding = true @@ -111,7 +115,7 @@ dependencies { implementation libs.google.gson // Third-party - annotationProcessor libs.dbflow.processor + kapt libs.dbflow.processor implementation libs.dbflow.core implementation libs.dbflow.lib @@ -136,7 +140,7 @@ dependencies { // Dependency Injection implementation libs.google.dagger - annotationProcessor libs.google.dagger.compiler + kapt libs.google.dagger.compiler // Logger implementation project(':applogger') @@ -153,7 +157,7 @@ dependencies { testImplementation libs.robolectric testImplementation libs.faker testImplementation libs.ooni.oonimkall - testAnnotationProcessor libs.google.dagger.compiler + kaptTest libs.google.dagger.compiler // Instrumentation Testing androidTestImplementation project(':shared-test') @@ -166,7 +170,7 @@ dependencies { androidTestImplementation libs.androidx.espresso.contrib androidTestImplementation libs.androidx.espresso.core androidTestImplementation libs.barista - androidTestAnnotationProcessor libs.google.dagger.compiler + kaptAndroidTest libs.google.dagger.compiler } static def versionCodeDate() { diff --git a/app/jacoco.gradle b/app/jacoco.gradle index f75812752..15b2b36c1 100644 --- a/app/jacoco.gradle +++ b/app/jacoco.gradle @@ -48,13 +48,19 @@ task jacocoAndroidTestReport(type: JacocoReport) { executionData.from += fileTree(dir: codeCoverageDataLocation, includes: ['**/*.ec']) } - reports { - html.enabled true - html.destination file("${buildDir}/reports/coverage") - xml.enabled true - xml.destination file("${buildDir}/reports/coverage.xml") - csv.enabled false - } + reports { + html { + enabled true + destination file("${buildDir}/reports/coverage") + } + xml { + enabled true + destination file("${buildDir}/reports/coverage.xml") + } + csv { + enabled false + } + } doLast { println "Wrote HTML coverage report to ${reports.html.destination}/index.html" diff --git a/gradle.properties b/gradle.properties index 8de505811..a8c570082 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,10 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. +android.defaults.buildfeatures.buildconfig=true android.enableJetifier=true +android.nonFinalResIds=false +android.nonTransitiveRClass=false android.useAndroidX=true org.gradle.jvmargs=-Xmx1536m # When configured, Gradle will run in incubating parallel mode. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e1737e670..df1da5b9d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,16 +1,16 @@ [versions] -androidGradlePlugin = "7.4.1" +androidGradlePlugin = "8.1.2" barista = "3.9.0" countlySdk = "23.6.0" faker = "1.2.8" mockitoCore = "5.3.1" mockitoInline = "4.6.1" -robolectric = "4.6.1" +robolectric = "4.10.3" fastlaneScreengrab = "2.0.0" sentryAndroid = "6.3.0" xanscaleLocalhostToolkit = "19.05.01" commonsIo = "2.6" -jacoco = "0.8.5" +jacoco = "0.8.7" kotlin = "1.8.0" # Android X @@ -29,7 +29,7 @@ androidxEspressoCore = "3.5.1" googleGson = "2.8.9" googleGuava = "30.1.1-android" googleMaterial = "1.6.1" -googleDagger = "2.36" +googleDagger = "2.45" googleFirebaseBon = "26.3.0" googlePlaycore = "1.10.3" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2ec77e51a..3a0290794 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/shared-test/build.gradle b/shared-test/build.gradle index 607a28dff..34d70a978 100644 --- a/shared-test/build.gradle +++ b/shared-test/build.gradle @@ -1,18 +1,22 @@ plugins { id 'com.android.library' + id 'kotlin-android' + id 'kotlin-kapt' } android { namespace 'org.openobservatory.ooniprobe.shared.test' - compileSdk 33 + compileSdk libs.versions.compileSdk.get().toInteger() defaultConfig { - minSdk 21 + minSdk libs.versions.minSdk.get().toInteger() } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 } flavorDimensions = ['testing', 'license'] productFlavors { @@ -41,12 +45,12 @@ dependencies { // Dependency Injection (https://dagger.dev/) implementation libs.google.dagger - annotationProcessor libs.google.dagger.compiler + kapt libs.google.dagger.compiler // Database Library (https://github.com/agrosner/DBFlow) implementation libs.dbflow.core implementation libs.dbflow.lib - annotationProcessor libs.dbflow.processor + kapt libs.dbflow.processor // Gson Serialization Library (https://github.com/google/gson) implementation libs.google.gson @@ -60,6 +64,7 @@ dependencies { implementation libs.retrofit.logging.interceptor implementation libs.androidx.appcompat + implementation libs.xanscale.localhost.toolkit testImplementation libs.junit4 androidTestImplementation libs.androidx.junit From 93d9740e8147db0f18cea02f43e1abce449e5807 Mon Sep 17 00:00:00 2001 From: Norbel Ambanumben Date: Thu, 26 Oct 2023 12:12:21 +0100 Subject: [PATCH 02/12] Updated `CustomWebsiteActivity` to use new theme and also conver item builder to use `RecyclerView` --- app/src/main/AndroidManifest.xml | 3 +- .../activity/CustomWebsiteActivity.java | 91 +++++++------------ .../CustomWebsiteRecyclerViewAdapter.kt | 75 +++++++++++++++ .../main/res/drawable/add_circle_outline.xml | 5 + .../drawable/ic_baseline_remove_circle_24.xml | 9 -- .../res/layout/activity_customwebsite.xml | 26 ++++-- app/src/main/res/layout/edittext_url.xml | 15 +-- app/src/main/res/values/styles.xml | 7 ++ 8 files changed, 153 insertions(+), 78 deletions(-) create mode 100644 app/src/main/java/org/openobservatory/ooniprobe/adapters/CustomWebsiteRecyclerViewAdapter.kt create mode 100644 app/src/main/res/drawable/add_circle_outline.xml delete mode 100644 app/src/main/res/drawable/ic_baseline_remove_circle_24.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1b717a3e5..15536b39d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -99,7 +99,8 @@ android:label="@string/Settings_Websites_CustomURL_Title" android:exported="false" android:parentActivityName=".activity.PreferenceActivity" - android:screenOrientation="userPortrait"> + android:screenOrientation="userPortrait" + android:theme="@style/Theme.MaterialComponents.Light.NoActionBar.CustomWebsite"> diff --git a/app/src/main/java/org/openobservatory/ooniprobe/activity/CustomWebsiteActivity.java b/app/src/main/java/org/openobservatory/ooniprobe/activity/CustomWebsiteActivity.java index bef139f13..20cd47558 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/activity/CustomWebsiteActivity.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/activity/CustomWebsiteActivity.java @@ -7,24 +7,30 @@ import android.view.ViewGroup; import android.widget.EditText; import android.widget.ImageButton; + import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; + import localhost.toolkit.app.fragment.ConfirmDialogFragment; + import org.openobservatory.ooniprobe.R; +import org.openobservatory.ooniprobe.adapters.CustomWebsiteRecyclerViewAdapter; import org.openobservatory.ooniprobe.common.PreferenceManager; import org.openobservatory.ooniprobe.databinding.ActivityCustomwebsiteBinding; import org.openobservatory.ooniprobe.model.database.Url; import org.openobservatory.ooniprobe.test.suite.WebsitesSuite; import javax.inject.Inject; + import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; public class CustomWebsiteActivity extends AbstractActivity implements ConfirmDialogFragment.OnConfirmedListener { - private ArrayList editTexts; - private ArrayList deletes; - @Inject PreferenceManager preferenceManager; + private CustomWebsiteRecyclerViewAdapter adapter; private ActivityCustomwebsiteBinding binding; @Override @@ -33,15 +39,14 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { getActivityComponent().inject(this); binding = ActivityCustomwebsiteBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - editTexts = new ArrayList<>(); - deletes = new ArrayList<>(); - binding.bottomBar.inflateMenu(R.menu.run); + + setSupportActionBar(binding.toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + binding.bottomBar.setOnMenuItemClickListener(item -> { - if (!checkPrefix()) - return false; - ArrayList urls = new ArrayList<>(editTexts.size()); - for (EditText editText : editTexts) { - String value = editText.getText().toString(); + List items = adapter.getItems(); + ArrayList urls = new ArrayList<>(items.size()); + for (String value : items) { String sanitizedUrl = value.replaceAll("\\r\\n|\\r|\\n", " "); //https://support.microsoft.com/en-us/help/208427/maximum-url-length-is-2-083-characters-in-internet-explorer if (Patterns.WEB_URL.matcher(sanitizedUrl).matches() && sanitizedUrl.length() < 2084) @@ -50,39 +55,27 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { WebsitesSuite suite = new WebsitesSuite(); suite.getTestList(preferenceManager)[0].setInputs(urls); - RunningActivity.runAsForegroundService(CustomWebsiteActivity.this, suite.asArray(), this::finish, preferenceManager); + RunningActivity.runAsForegroundService(CustomWebsiteActivity.this, suite.asArray(), this::finish,preferenceManager); return true; }); binding.add.setOnClickListener(v -> add()); + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + binding.urlContainer.setLayoutManager(layoutManager); + adapter = new CustomWebsiteRecyclerViewAdapter(input -> { + binding.bottomBar.setTitle(getString(R.string.OONIRun_URLs, Integer.toString(adapter.getItemCount()))); + }); + binding.urlContainer.setAdapter(adapter); add(); } @Override public void onBackPressed() { String base = getString(R.string.http); - boolean edited = false; - for (EditText editText : editTexts) - if (!editText.getText().toString().equals(base)) { - edited = true; - break; - } - if (edited) - new ConfirmDialogFragment.Builder() - .withMessage(getString(R.string.Modal_CustomURL_NotSaved)) - .build().show(getSupportFragmentManager(), null); - else - super.onBackPressed(); - } + boolean edited = adapter.getItemCount() > 0 && !adapter.getItems().get(0).equals(base); - public boolean checkPrefix(){ - boolean prefix = true; - for (EditText editText : editTexts) - if (!editText.getText().toString().contains("http://") - && !editText.getText().toString().contains("https://")) { - prefix = false; - editText.setError(getString(R.string.Settings_Websites_CustomURL_NoURLEntered)); - } - return prefix; + if (edited) + new ConfirmDialogFragment.Builder().withMessage(getString(R.string.Modal_CustomURL_NotSaved)).build().show(getSupportFragmentManager(), null); + else super.onBackPressed(); } @Override @@ -92,33 +85,19 @@ public boolean onSupportNavigateUp() { } void add() { - ViewGroup urlBox = (ViewGroup) getLayoutInflater().inflate(R.layout.edittext_url, binding.urlContainer, false); - EditText editText = urlBox.findViewById(R.id.editText); - editTexts.add(editText); - binding.urlContainer.addView(urlBox); - ImageButton delete = urlBox.findViewById(R.id.delete); - deletes.add(delete); - delete.setTag(editText); - delete.setOnClickListener(v -> { - EditText tag = (EditText) v.getTag(); - ((View) v.getParent()).setVisibility(View.GONE); - editTexts.remove(tag); - deletes.remove(v); - binding.bottomBar.setTitle(getString(R.string.OONIRun_URLs, Integer.toString(editTexts.size()))); - setVisibilityDelete(); - }); - setVisibilityDelete(); - binding.bottomBar.setTitle(getString(R.string.OONIRun_URLs, Integer.toString(editTexts.size()))); + adapter.addAll(Collections.singletonList(getString(R.string.http))); + binding.bottomBar.setTitle(getString(R.string.OONIRun_URLs, Integer.toString(adapter.getItemCount()))); + adapter.notifyDataSetChanged(); + this.scrollToBottom(); } - private void setVisibilityDelete() { - for (ImageButton delete : deletes) - delete.setVisibility(deletes.size() > 1 ? View.VISIBLE : View.INVISIBLE); + void scrollToBottom() { + binding.urlContainer.scrollToPosition(adapter.getItemCount() - 1); + binding.urlsList.post(() -> binding.urlsList.fullScroll(View.FOCUS_DOWN)); } @Override public void onConfirmation(Serializable serializable, int i) { - if (i == DialogInterface.BUTTON_POSITIVE) - super.onBackPressed(); + if (i == DialogInterface.BUTTON_POSITIVE) super.onBackPressed(); } } diff --git a/app/src/main/java/org/openobservatory/ooniprobe/adapters/CustomWebsiteRecyclerViewAdapter.kt b/app/src/main/java/org/openobservatory/ooniprobe/adapters/CustomWebsiteRecyclerViewAdapter.kt new file mode 100644 index 000000000..95dfc3280 --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/adapters/CustomWebsiteRecyclerViewAdapter.kt @@ -0,0 +1,75 @@ +package org.openobservatory.ooniprobe.adapters + +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.openobservatory.ooniprobe.databinding.EdittextUrlBinding +import java.lang.Boolean.TRUE + + +class CustomWebsiteRecyclerViewAdapter(private val onItemRemovedListener: ItemRemovedListener) : + RecyclerView.Adapter() { + private val mItems: MutableList + private val mVisibility: MutableList + + /** + * Initialize the dataset of the Adapter. + */ + init { + mItems = ArrayList() + mVisibility = ArrayList() + } + + fun addAll(items: List?) { + mItems.addAll(items ?: listOf()) + mVisibility.addAll(mItems.map { TRUE }) + mVisibility[0] = mItems.size > 1 + } + + override fun onCreateViewHolder( + parent: ViewGroup, viewType: Int + ): ViewHolder { + return ViewHolder( + EdittextUrlBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder( + holder: ViewHolder, position: Int + ) { + holder.binding.editText.setText(mItems[position]) + holder.binding.delete.visibility = + if (mVisibility[position]) View.VISIBLE else View.INVISIBLE + holder.binding.delete.setOnClickListener { + mItems.removeAt(holder.adapterPosition) + mVisibility.removeAt(holder.adapterPosition) + mVisibility[0] = mItems.size > 1 + notifyDataSetChanged() + onItemRemovedListener.onItemRemoved(holder.adapterPosition) + } + holder.binding.editText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} + override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) { + mItems[holder.adapterPosition] = charSequence.toString() + } + + override fun afterTextChanged(editable: Editable) {} + }) + } + + override fun getItemCount(): Int = mItems.size + fun getItems(): List = mItems + + class ViewHolder(val binding: EdittextUrlBinding) : RecyclerView.ViewHolder(binding.root) +} + +interface ItemRemovedListener { + fun onItemRemoved(position: Int) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/add_circle_outline.xml b/app/src/main/res/drawable/add_circle_outline.xml new file mode 100644 index 000000000..9b10da783 --- /dev/null +++ b/app/src/main/res/drawable/add_circle_outline.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_remove_circle_24.xml b/app/src/main/res/drawable/ic_baseline_remove_circle_24.xml deleted file mode 100644 index 3ce4ba381..000000000 --- a/app/src/main/res/drawable/ic_baseline_remove_circle_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout/activity_customwebsite.xml b/app/src/main/res/layout/activity_customwebsite.xml index 2c0786840..5a2fde126 100644 --- a/app/src/main/res/layout/activity_customwebsite.xml +++ b/app/src/main/res/layout/activity_customwebsite.xml @@ -1,9 +1,22 @@ + android:orientation="vertical" + tools:context=".activity.CustomWebsiteActivity"> + + + + - + tools:listitem="@layout/edittext_url"/>