diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..37a54fbe7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +version: 2 +updates: + - package-ecosystem: gradle + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 diff --git a/.github/workflows/archive.yml b/.github/workflows/archive.yml deleted file mode 100644 index b04392ac3..000000000 --- a/.github/workflows/archive.yml +++ /dev/null @@ -1,19 +0,0 @@ -# archive creates and publishes an apk for testing -name: archive -on: [push] -jobs: - build: - runs-on: macos-latest - steps: - - uses: actions/setup-java@v2 - with: - java-version: '17' - distribution: 'temurin' - - name: checkout - uses: actions/checkout@v2 - - run: ./gradlew clean assembleDevFullRelease - - name: uploads dev apk - uses: actions/upload-artifact@v3 - with: - name: dev-apk - path: app/build/outputs/apk/devFull/release diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 46f49645d..000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,19 +0,0 @@ -# build checks whether the project builds -name: build -on: [push] -jobs: - build: - runs-on: macos-latest - strategy: - matrix: - version: - - "StableFullRelease" - - "StableFdroidRelease" - steps: - - uses: actions/setup-java@v2 - with: - java-version: '17' - distribution: 'temurin' - - name: checkout - uses: actions/checkout@v2 - - run: ./gradlew build${{ matrix.version }} && ./gradlew clean diff --git a/.github/workflows/emulator.yml b/.github/workflows/emulator.yml deleted file mode 100644 index de92c9393..000000000 --- a/.github/workflows/emulator.yml +++ /dev/null @@ -1,38 +0,0 @@ -# emulator runs tests inside the android emulator -name: emulator -on: - push: - branches: - - master - pull_request: - schedule: - - cron: "0 2 * * */2" -jobs: - test: - runs-on: macos-latest - strategy: - matrix: - api-level: [29] - target: [google_apis] - steps: - - uses: actions/setup-java@v2 - with: - java-version: '17' - distribution: 'temurin' - - name: checkout - uses: actions/checkout@v2 - - name: run tests - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ matrix.api-level }} - target: ${{ matrix.target }} - arch: x86_64 - profile: Nexus 6 - ram-size: 2048M - script: ./gradlew connectedStableFullDebugAndroidTest - - name: uploads test results - uses: actions/upload-artifact@v2 - if: failure() - with: - name: emulator-test-results - path: app/build/reports/androidTests/connected/flavors/stableFullDebugAndroidTest diff --git a/.github/workflows/firebase-app-distribution.yml b/.github/workflows/firebase-app-distribution.yml deleted file mode 100644 index c55cc486c..000000000 --- a/.github/workflows/firebase-app-distribution.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Build & upload to Firebase App Distribution - -on: [push] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Resolve PR number - uses: jwalton/gh-find-current-pr@v1 - id: findPr - with: - # Can be "open", "closed", or "all". Defaults to "open". - state: open - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - name: Build `DevFullDebug` variant - if: success() && steps.findPr.outputs.number - run: ./gradlew clean assembleDevFullDebug - env: - PR_NUMBER: ${{ steps.findPr.outputs.pr }} - - name: Upload artifact to Firebase App Distribution - uses: wzieba/Firebase-Distribution-Github-Action@v1.7.0 - id: uploadArtifact - with: - appId: ${{secrets.FIREBASE_APP_ID}} - serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }} - groups: testers - file: app/build/outputs/apk/devFull/debug/app-dev-full-debug.apk - - name: Write Summary - run: | - echo "View this release in the Firebase console: ${{ steps.uploadArtifact.outputs.FIREBASE_CONSOLE_URI }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index ef0bc109f..000000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,20 +0,0 @@ -# test runs unit tests using the JVM -name: test -on: [push] -jobs: - test: - runs-on: macos-latest - steps: - - uses: actions/setup-java@v2 - with: - java-version: '17' - distribution: 'temurin' - - name: checkout - uses: actions/checkout@v2 - - run: ./gradlew testStableFullRelease - - name: uploads test results - uses: actions/upload-artifact@v2 - if: failure() - with: - name: test-results - path: app/build/reports/tests/testStableFullReleaseUnitTest diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 000000000..a16aa1846 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,180 @@ +# This workflow is triggered on push events to the repository +# It runs the following jobs: +# - build: Ensure the code builds +# - unit-test: Run unit tests +# - instrumented-test: Run instrumented tests +# - assemble-archive: Archive APKs +# - distribute: Upload artifact to Firebase App Distribution +name: Validate +on: [ push ] +jobs: + build: + name: Ensure the code builds + runs-on: ubuntu-latest + + strategy: + matrix: + version: + - "StableFullRelease" + - "StableFdroidRelease" + + steps: + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: checkout + uses: actions/checkout@v4 + + - name: Build `StableFullRelease` and `StableFdroidRelease` variants + run: ./gradlew build${{ matrix.version }} && ./gradlew clean + + unit-test: + name: Run unit tests + runs-on: ubuntu-latest + needs: [ build ] + + steps: + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: checkout + uses: actions/checkout@v4 + + - name: Run unit tests + run: ./gradlew testStableFullRelease + + - name: Uploads test reports + uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-report + path: app/build/test-results/testStableFullDebugUnitTest + + instrumented-test: + name: Run instrumented tests + runs-on: ubuntu-latest + needs: [ build ] + + strategy: + matrix: + api-level: [ 29 ] + target: [ google_apis ] + + steps: + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: checkout + uses: actions/checkout@v4 + + - name: Run instrumented tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + arch: x86_64 + profile: pixel_3_xl + ram-size: 4096M + disable-animations: true + script: ./gradlew connectedStableFullDebugAndroidTest + + - name: uploads test reports + uses: actions/upload-artifact@v4 + if: failure() + with: + name: emulator-test-reports + path: app/build/reports/androidTests/connected/debug/flavors/stableFull/ + + assemble-archive: + name: Archive APKs + runs-on: ubuntu-latest + + strategy: + matrix: + version: + - "StableFullDebug" + - "StableFdroidDebug" + - "DevFullDebug" + - "DevFullDebugAndroidTest" + needs: [ build ] + + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: Get issue number + uses: actions/github-script@v6 + id: get_issue_number + with: + script: | + if (context.issue.number) { + // Return issue number if present + return context.issue.number; + } else { + // Otherwise return issue number from commit + return ( + await github.rest.repos.listPullRequestsAssociatedWithCommit({ + commit_sha: context.sha, + owner: context.repo.owner, + repo: context.repo.repo, + }) + ).data[0].number; + } + result-encoding: string + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Assemble APKs + if: success() && steps.get_issue_number.outputs.result + run: ./gradlew assemble${{ matrix.version }} + env: + PR_NUMBER: ${{ steps.get_issue_number.outputs.result }} + + - name: uploads dev apk + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.version }}Apk + path: | + app/build/outputs/apk/devFull/debug/app-dev-full-debug.apk + app/build/outputs/apk/androidTest/devFull/debug/app-dev-full-debug-androidTest.apk + app/build/outputs/apk/stableFull/debug/app-stable-full-debug.apk + app/build/outputs/apk/stableFdroid/debug/app-stable-fdroid-debug.apk + + distribute: + name: Upload artifact to Firebase App Distribution + runs-on: ubuntu-latest + needs: [ assemble-archive ] + steps: + + - name: checkout + uses: actions/checkout@v4 + + - name: Download app APK + uses: actions/download-artifact@v4 + with: + name: DevFullDebugApk + + - name: Upload artifact to Firebase App Distribution + uses: wzieba/Firebase-Distribution-Github-Action@v1.7.0 + id: uploadArtifact + with: + appId: ${{secrets.FIREBASE_APP_ID}} + serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }} + groups: testers + file: devFull/debug/app-dev-full-debug.apk + - name: Write Summary + run: | + echo "View this release in the Firebase console: ${{ steps.uploadArtifact.outputs.FIREBASE_CONSOLE_URI }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/app/src/androidTest/java/org/openobservatory/ooniprobe/ui/OoniRunActivityTest.java b/app/src/androidTest/java/org/openobservatory/ooniprobe/ui/OoniRunActivityTest.java index e05f12d58..b6482ef0c 100644 --- a/app/src/androidTest/java/org/openobservatory/ooniprobe/ui/OoniRunActivityTest.java +++ b/app/src/androidTest/java/org/openobservatory/ooniprobe/ui/OoniRunActivityTest.java @@ -14,15 +14,17 @@ import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; public class OoniRunActivityTest extends AbstractTest { + String version = BuildConfig.VERSION_NAME.split("-")[0]; + private ActivityScenario scenario; @Test public void openValid() { - String version = BuildConfig.VERSION_NAME.split("-")[0]; scenario = launch("ooni://nettest?mv=" + version + "&tn=dash"); onView(withText(R.string.Test_Dash_Fullname)).check(matches(isDisplayed())); scenario.close(); @@ -30,12 +32,80 @@ public void openValid() { @Test public void openValidWithUrs() { - String version = BuildConfig.VERSION_NAME.split("-")[0]; scenario = launch( - "ooni://nettest?mv=" + version + "&tn=web_connectivity&ta={\"urls\":[\"http://example.org\"]}" + "ooni://nettest?mv=" + version + "&tn=web_connectivity&ta={\"urls\":[\"http://example.org\"]}" ); onView(withText(R.string.Test_WebConnectivity_Fullname)).check(matches(isDisplayed())); onView(withText("http://example.org")).check(matches(isDisplayed())); + checkRunButtonIsDisplayed(); + scenario.close(); + } + + @Test + public void openPartialInputs() { + scenario = launch("ooni://nettest?tn=web_connectivity&ta=%7B%22urls%22%3A%5B%22http%3A%2F%2F%22%5D%7D&mv=" + version); + onView(withText(R.string.Test_WebConnectivity_Fullname)).check(matches(isDisplayed())); + onView(withText("http://")).check(matches(isDisplayed())); + checkRunButtonIsDisplayed(); + scenario.close(); + } + + @Test + public void openValidUrls() { + scenario = launch("ooni://nettest?tn=web_connectivity&ta=%7B%22urls%22%3A%5B%22http%3A%2F%2Fwww.google.it%22%2C%22https%3A%2F%2Frun.ooni.io%2F%22%5D%7D&mv=" + version); + onView(withText(R.string.Test_WebConnectivity_Fullname)).check(matches(isDisplayed())); + onView(withText("http://www.google.it")).check(matches(isDisplayed())); + onView(withText("https://run.ooni.io/")).check(matches(isDisplayed())); + checkRunButtonIsDisplayed(); + scenario.close(); + } + + @Test + public void openMalformedUrl() { + scenario = launch("ooni://nettest?tn=web_connectivity&ta=%7B%22urls%22%3A%5B%22http%3A%2F%2Fwww.google.it%22%2C%22https%3A%2F%2Frun.ooni.io&mv=" + version); + onView(withText(R.string.OONIRun_InvalidParameter)).check(matches(isDisplayed())); + onView(withText(R.string.OONIRun_InvalidParameter_Msg)).check(matches(isDisplayed())); + checkRunButtonIsSetToCloseView(); + scenario.close(); + } + + @Test + public void openNdt() { + scenario = launch("ooni://nettest?tn=ndt&mv=" + version); + onView(withText(R.string.Test_NDT_Fullname)).check(matches(isDisplayed())); + checkRunButtonIsDisplayed(); + scenario.close(); + } + + @Test + public void openDash() { + scenario = launch("ooni://nettest?tn=dash&mv=" + version); + onView(withText(R.string.Test_Dash_Fullname)).check(matches(isDisplayed())); + checkRunButtonIsDisplayed(); + scenario.close(); + } + + @Test + public void openHttpInvalidRequestLine() { + scenario = launch("ooni://nettest?tn=http_invalid_request_line&mv=" + version); + onView(withText(R.string.Test_HTTPInvalidRequestLine_Fullname)).check(matches(isDisplayed())); + checkRunButtonIsDisplayed(); + scenario.close(); + } + + @Test + public void openHttpHeaderFieldManipulation() { + scenario = launch("ooni://nettest?tn=http_header_field_manipulation&mv=" + version); + onView(withText(R.string.Test_HTTPHeaderFieldManipulation_Fullname)).check(matches(isDisplayed())); + checkRunButtonIsDisplayed(); + scenario.close(); + } + + @Test + public void openInvalidTestName() { + scenario = launch("ooni://nettest?tn=antani&mv=" + version); + onView(withText(R.string.OONIRun_InvalidParameter)).check(matches(isDisplayed())); + checkRunButtonIsSetToCloseView(); scenario.close(); } @@ -43,9 +113,25 @@ public void openValidWithUrs() { public void openOutdatedVersion() { scenario = launch("ooni://nettest?mv=2100.01.01&tn=dash"); onView(withText(R.string.OONIRun_OONIProbeOutOfDate)).check(matches(isDisplayed())); + onView(withId(R.id.run)) + .check(matches(withText(R.string.OONIRun_Update))) + .check(matches(isDisplayed())); scenario.close(); } + private static void checkRunButtonIsDisplayed() { + onView(withId(R.id.run)) + .check(matches(withText(R.string.OONIRun_Run))) + .check(matches(isDisplayed())); + } + + private static void checkRunButtonIsSetToCloseView() { + onView(withId(R.id.run)) + .check(matches(withText(R.string.OONIRun_Close))) + .check(matches(isDisplayed())); + } + + private ActivityScenario launch(String uri) { return ActivityScenario.launch(new Intent(Intent.ACTION_VIEW, Uri.parse(uri))); } 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..3b6b7a371 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/common/Application.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/common/Application.java @@ -43,7 +43,10 @@ public class Application extends android.app.Application { ExecutorService executorService = Executors.newFixedThreadPool(4); public AppComponent component; - @Inject AppLogger logger; + @Inject AppLogger logger; + + @Inject + TestStateRepository testStateRepository; @Override public void onCreate() { super.onCreate(); @@ -116,6 +119,9 @@ public AppLogger getLogger() { return logger; } + public TestStateRepository getTestStateRepository() { + return testStateRepository; + } public Gson getGson() { return _gson; } diff --git a/app/src/main/java/org/openobservatory/ooniprobe/common/TestStateRepository.kt b/app/src/main/java/org/openobservatory/ooniprobe/common/TestStateRepository.kt new file mode 100644 index 000000000..849c6451c --- /dev/null +++ b/app/src/main/java/org/openobservatory/ooniprobe/common/TestStateRepository.kt @@ -0,0 +1,19 @@ +package org.openobservatory.ooniprobe.common + +import androidx.lifecycle.MutableLiveData +import javax.inject.Inject +import javax.inject.Singleton + +/** + * This class is used to store the state of the test group. + * It is used to communicate the state of the test group between the [org.openobservatory.ooniprobe.test.TestAsyncTask] and any [androidx.lifecycle.Observer]. + * + * Note: Ideally, this class should used to replace Broadcasts from [org.openobservatory.ooniprobe.test.TestAsyncTask] + * and "org.openobservatory.ooniprobe.activity.RunningActivity" broadcast events + */ +@Singleton +class TestStateRepository @Inject constructor() { + var testGroupStatus = MutableLiveData(TestGroupStatus.NOT_STARTED) +} + +enum class TestGroupStatus { NOT_STARTED, RUNNING, FINISHED } \ No newline at end of file diff --git a/app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.kt b/app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.kt index da7ab2827..0e6ca912b 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.kt +++ b/app/src/main/java/org/openobservatory/ooniprobe/fragment/DashboardFragment.kt @@ -13,19 +13,18 @@ import org.openobservatory.engine.BaseNettest import org.openobservatory.ooniprobe.R import org.openobservatory.ooniprobe.activity.AbstractActivity import org.openobservatory.ooniprobe.activity.OverviewActivity -import org.openobservatory.ooniprobe.activity.RunningActivity import org.openobservatory.ooniprobe.activity.runtests.RunTestsActivity import org.openobservatory.ooniprobe.adapters.DashboardAdapter import org.openobservatory.ooniprobe.common.AbstractDescriptor import org.openobservatory.ooniprobe.common.Application -import org.openobservatory.ooniprobe.common.OONIDescriptor import org.openobservatory.ooniprobe.common.PreferenceManager import org.openobservatory.ooniprobe.common.ReachabilityManager +import org.openobservatory.ooniprobe.common.TestGroupStatus +import org.openobservatory.ooniprobe.common.TestStateRepository import org.openobservatory.ooniprobe.common.ThirdPartyServices import org.openobservatory.ooniprobe.databinding.FragmentDashboardBinding import org.openobservatory.ooniprobe.fragment.dashboard.DashboardViewModel import org.openobservatory.ooniprobe.model.database.Result -import org.openobservatory.ooniprobe.test.suite.AbstractSuite import javax.inject.Inject class DashboardFragment : Fragment(), View.OnClickListener { @@ -36,6 +35,8 @@ class DashboardFragment : Fragment(), View.OnClickListener { lateinit var viewModel: DashboardViewModel private var descriptors: ArrayList> = ArrayList() + @Inject + lateinit var testStateRepository: TestStateRepository private lateinit var binding: FragmentDashboardBinding @@ -74,6 +75,16 @@ class DashboardFragment : Fragment(), View.OnClickListener { addAll(items) } } + + testStateRepository.testGroupStatus.observe(viewLifecycleOwner) { status -> + if (status == TestGroupStatus.RUNNING) { + binding.runAll.visibility = View.GONE + binding.lastTested.visibility = View.GONE + } else { + binding.runAll.visibility = View.VISIBLE + binding.lastTested.visibility = View.VISIBLE + } + } } override fun onResume() { diff --git a/app/src/main/java/org/openobservatory/ooniprobe/fragment/ProgressFragment.kt b/app/src/main/java/org/openobservatory/ooniprobe/fragment/ProgressFragment.kt index a5865df63..7de0bbce5 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/fragment/ProgressFragment.kt +++ b/app/src/main/java/org/openobservatory/ooniprobe/fragment/ProgressFragment.kt @@ -149,16 +149,20 @@ class ProgressFragment : Fragment() { override fun onPause() { super.onPause() - if (receiver.isBound) { - requireContext().unbindService(receiver) - receiver.isBound = false + if (::receiver.isInitialized) { + if (receiver.isBound) { + requireContext().unbindService(receiver) + receiver.isBound = false + } + LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(receiver) } - LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(receiver) } override fun onDestroy() { super.onDestroy() - LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(receiver) + if (::receiver.isInitialized) { + LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(receiver) + } } private inner class TestRunnerEventListener : TestRunBroadRequestReceiver.EventListener { 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 ee982a44a..985e46b72 100644 --- a/app/src/main/java/org/openobservatory/ooniprobe/test/TestAsyncTask.java +++ b/app/src/main/java/org/openobservatory/ooniprobe/test/TestAsyncTask.java @@ -23,6 +23,7 @@ import org.openobservatory.ooniprobe.common.ListUtility; import org.openobservatory.ooniprobe.common.MKException; import org.openobservatory.ooniprobe.common.PreferenceManager; +import org.openobservatory.ooniprobe.common.TestGroupStatus; import org.openobservatory.ooniprobe.common.ThirdPartyServices; import org.openobservatory.ooniprobe.common.service.RunTestService; import org.openobservatory.ooniprobe.common.service.ServiceUtil; @@ -107,6 +108,7 @@ private void unregisterConnChange() { @Override protected Void doInBackground(Void... voids) { + app.getTestStateRepository().getTestGroupStatus().postValue(TestGroupStatus.RUNNING); if (app != null && testSuites != null) { registerConnChange(); for (int suiteIdx = 0; suiteIdx < testSuites.size(); suiteIdx++) { @@ -125,6 +127,7 @@ protected Void doInBackground(Void... voids) { } } } + app.getTestStateRepository().getTestGroupStatus().postValue(TestGroupStatus.FINISHED); } return null; } diff --git a/app/src/main/res/drawable/progress_blue.xml b/app/src/main/res/drawable/progress_blue.xml index 2d700aab2..f26274249 100644 --- a/app/src/main/res/drawable/progress_blue.xml +++ b/app/src/main/res/drawable/progress_blue.xml @@ -2,7 +2,7 @@ - + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 0cb487282..2996af740 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -11,12 +11,6 @@ android:layout_height="0dp" android:layout_weight="1"/> - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_overview.xml b/app/src/main/res/layout/activity_overview.xml index 4efea4b0a..cb192a83f 100644 --- a/app/src/main/res/layout/activity_overview.xml +++ b/app/src/main/res/layout/activity_overview.xml @@ -216,12 +216,4 @@ tools:visibility="visible"/> - - diff --git a/app/src/main/res/layout/activity_result_detail.xml b/app/src/main/res/layout/activity_result_detail.xml index 83726362c..eaa550612 100644 --- a/app/src/main/res/layout/activity_result_detail.xml +++ b/app/src/main/res/layout/activity_result_detail.xml @@ -70,11 +70,4 @@ android:id="@+id/snackbarAnchor" android:layout_width="match_parent" android:layout_height="wrap_content"/> - - diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml index d90d20ad8..02de950e0 100644 --- a/app/src/main/res/layout/fragment_dashboard.xml +++ b/app/src/main/res/layout/fragment_dashboard.xml @@ -30,13 +30,12 @@ - - + -