diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f63ab047..3da3ecbe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ on: jobs: build: - name: ${{ matrix.os }}, java-${{ matrix.java_version }}, node-${{ matrix.node_version }} + name: Build ${{ matrix.os }}, java-${{ matrix.java_version }}, node-${{ matrix.node_version }} runs-on: ${{ matrix.os }} env: SUPPLY_TRACK: production # used by fastlane to determine track to publish to @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-20.04] - java_version: [1.8] + java_version: [11] node_version: [16] ruby_version: ['3.0'] ndk_version: ['21.4.7075529'] @@ -119,12 +119,7 @@ jobs: make aw-webui # Build or fetch aw-server-rust artifacts - - name: Build aw-server-rust - if: ${{ matrix.rust_build }} - run: | - make aw-server-rust - - - name: Download artifact + - name: Download prebuilt aw-server-rust Android libs uses: dawidd6/action-download-artifact@v2 if: ${{ !matrix.rust_build }} with: @@ -144,6 +139,16 @@ jobs: # "completed", "in_progress", "queued" # Default: "completed" workflow_conclusion: success + - name: Build aw-server-rust + env: + USE_PREBUILT: ${{ !matrix.rust_build }} + run: | + # will build if USE_PREBUILT is true, + # otherwise will just move files into the right place + make aw-server-rust + - name: Check that jniLibs present + run: | + test -e mobile/src/main/jniLibs/x86_64/libaw_server.so - name: Set version if: startsWith(github.ref, 'refs/tags/v') # only on runs triggered from tag @@ -154,10 +159,15 @@ jobs: mobile/build.gradle bundle exec fastlane update_version + - name: Assemble debug & test APK + run: | + make build-apk-debug + # Install age & load secrets - name: Install age uses: adnsio/setup-age-action@v1.2.0 - name: Load secrets + if: env.KEY_ANDROID_JKS != null env: KEY_ANDROID_JKS: ${{ secrets.KEY_ANDROID_JKS }} run: | @@ -170,8 +180,6 @@ jobs: JKS_STOREPASS: ${{ secrets.KEY_ANDROID_JKS_STOREPASS }} JKS_KEYPASS: ${{ secrets.KEY_ANDROID_JKS_KEYPASS }} run: | - # TODO: Add related stuff in .travis.yml - # TODO: Allow building even if secrets not set make dist/aw-android.apk - name: Upload artifact @@ -180,6 +188,202 @@ jobs: name: aw-android path: dist/aw-android*.apk + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: apks + path: dist/apk/ + + test-e2e: + needs: [build] + name: Test E2E ${{ matrix.android_avd }} #-${{ matrix.os }}-eAPI-${{ matrix.android_emu_version }}-java-${{ matrix.java_version }}-node-${{ matrix.node_version }} + runs-on: ${{ matrix.os }} + env: + MATRIX_E_SDK: ${{ matrix.android_emu_version }} + MATRIX_AVD: ${{ matrix.android_avd }} + strategy: + fail-fast: false + max-parallel: 1 + matrix: + os: [macos-12] # macOS-latest, + android_avd: [Pixel_API_27_AOSP] + java_version: [11] + ndk_version: ['21.4.7075529'] + include: + - android_avd: Pixel_API_27_AOSP + android_emu_version: 27 + # # # Cannot run > 27-emuLevel -_- https://github.com/actions/runner-images/issues/6527 + # - android_avd: Pixel_API_32_AOSP + # android_emu_version: 32 + steps: + - uses: actions/checkout@v2 + with: + submodules: 'recursive' + + # Will download all artifacts to path + - name: Download build artifacts + uses: actions/download-artifact@v3 + with: + name: apks + path: dist/apk + - name: Display structure of downloaded files + working-directory: dist + run: ls -R + + # # # Below code is majorly from https://github.com/actions/runner-images/issues/6152#issuecomment-1243718140 + - name: Create Android emulator + run: | + brew install intel-haxm + # Install AVD files + echo "y" | $ANDROID_HOME/tools/bin/sdkmanager --install 'system-images;android-'$MATRIX_E_SDK';default;x86_64' + echo "y" | $ANDROID_HOME/tools/bin/sdkmanager --licenses + + # Create emulator + $ANDROID_HOME/tools/bin/avdmanager create avd -n $MATRIX_AVD -d pixel --package 'system-images;android-'$MATRIX_E_SDK';default;x86_64' + $ANDROID_HOME/emulator/emulator -list-avds + if false; then + emulator_config=~/.android/avd/$MATRIX_AVD.avd/config.ini + # The following madness is to support empty OR populated config.ini files, + # the state of which is dependant on the version of the emulator used (which we don't control), + # so let's be defensive to be safe. + # Replace existing config (NOTE we're on MacOS so sed works differently!) + sed -i .bak 's/hw.lcd.density=.*/hw.lcd.density=420/' "$emulator_config" + sed -i .bak 's/hw.lcd.height=.*/hw.lcd.height=1920/' "$emulator_config" + sed -i .bak 's/hw.lcd.width=.*/hw.lcd.width=1080/' "$emulator_config" + # Or, add new config + if ! grep -q "hw.lcd.density" "$emulator_config"; then + echo "hw.lcd.density=420" >> "$emulator_config" + fi + if ! grep -q "hw.lcd.height" "$emulator_config"; then + echo "hw.lcd.height=1920" >> "$emulator_config" + fi + if ! grep -q "hw.lcd.width" "$emulator_config"; then + echo "hw.lcd.width=1080" >> "$emulator_config" + fi + echo "Emulator settings ($emulator_config)" + cat "$emulator_config" + fi + + - name: Start Android emulator + timeout-minutes: 30 # ~4min normal - 3x DOSafety + env: + SUFFIX: ${{ matrix.android_avd }}-eAPI-${{ matrix.android_emu_version }}-${{ matrix.os }} + HOMEBREW_NO_INSTALL_CLEANUP: 1 + run: | + echo "Starting emulator and waiting for boot to complete...." + ls -la $ANDROID_HOME/emulator + nohup $ANDROID_HOME/tools/emulator -avd $MATRIX_AVD -gpu host -no-audio -no-boot-anim -camera-back none -camera-front none -qemu -m 2048 2>&1 & + $ANDROID_HOME/platform-tools/adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed | tr -d '\r') ]]; do echo "wait..."; sleep 1; done; input keyevent 82' + echo "Emulator has finished booting" + $ANDROID_HOME/platform-tools/adb devices + sleep 30 + mkdir -p screenshots + screencapture screenshots/screenshot-$SUFFIX.jpg + $ANDROID_HOME/platform-tools/adb exec-out screencap -p > screenshots/emulator-$SUFFIX.png + + # # # Have to re-setup everything since we need to run emulator for faster performance on masOS ? Other os'es emulator will not startup ? + # TODO: Optimize the steps taking into consideration all software present by default on macOS runner image + + # # # Test # # reactiveCircus is giving a black screenshot not working + # # # TODO: Take a screenshot of OS to confirm if its Emulator issue or testcode/androidsdk issue - or maybe the emulator is screen off ? + # # # https://github.com/ReactiveCircus/android-emulator-runner + # - name: Test Cache + # uses: reactivecircus/android-emulator-runner@v2 + # with: + # api-level: ${{ matrix.android_emu_version }} + # arch: x86_64 + # profile: Nexus 6 + # target: google_apis + # emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim + # script: echo Meoooow ! + # - name: Test + # id: test + # uses: reactivecircus/android-emulator-runner@v2 + # with: + # api-level: ${{ matrix.android_emu_version }} + # arch: x86_64 + # profile: Nexus 6 + # target: google_apis + # emulator-options: -gpu swiftshader_indirect -noaudio -no-boot-anim -no-snapshot-save + # # Only running specific Instrumentation tests cause others are failing right now. TODO: Fix others + # script: ./gradlew connectedCheck -Pandroid.testInstrumentationRunnerArguments.class=net.activitywatch.android.ScreenshotTest --stacktrace + + # - name: Install recorder and record session + # env: + # SUFFIX: ${{ matrix.android_avd }}-eAPI-${{ matrix.android_emu_version }}-${{ matrix.os }} + # run: | + # brew install ffmpeg + # $ANDROID_HOME/tools/emulator -help-all + # # -logcat *:v + # # $ANDROID_HOME/tools/emulator -port 18725 -verbose -no-audio -gpu swiftshader_indirect -logcat *:v @$MATRIX_AVD & + # ffmpeg -f avfoundation -i 0 -t 120 out$SUFFIX.mov & + + - name: Test App + timeout-minutes: 20 + id: test + run: | + adb install dist/apk/debug/mobile-debug.apk + adb install dist/apk/androidTest/debug/mobile-debug-androidTest.apk + adb shell pm list instrumentation + adb shell am instrument -w \ + -e class net.activitywatch.android.ScreenshotTest \ + net.activitywatch.android.debug.test/androidx.test.runner.AndroidJUnitRunner + + - name: Output and save logcat to file + if: ${{ success() || steps.test.conclusion == 'failure'}} + run: | + mkdir -p mobile/build + adb logcat -d > mobile/build/logcat.log + adb logcat -v color & + + - name: Screenshot + if: ${{ success() || steps.test.conclusion == 'failure'}} + env: + SUFFIX: ${{ matrix.android_avd }}-eAPI-${{ matrix.android_emu_version }}-${{ matrix.os }} + run: | + adb shell monkey -p net.activitywatch.android.debug 1 + sleep 10 + screencapture screenshots/pscreenshot-$SUFFIX.jpg + $ANDROID_HOME/platform-tools/adb exec-out screencap -p > screenshots/pemulator-$SUFFIX.png + ls -alh screenshots/ + + - name: Upload logcat + if: ${{ success() || steps.test.conclusion == 'failure'}} + uses: actions/upload-artifact@v3 + with: + name: logcat + # mobile\build\outputs\connected_android_test_additional_output\debugAndroidTest\connected\Pixel_XL_API_32(AVD) - 12\ScreenshotTest_saveDeviceScreenBitmap.png + path: | + mobile/build/logcat.log + #mobile/build/reports/* + + # - name: Upload video + # if: ${{ success() || steps.test.conclusion == 'failure'}} + # uses: actions/upload-artifact@master + # with: + # name: video + # path: ./*.mov # out.mov + + - name: Upload screenshots + uses: actions/upload-artifact@v3 + if: ${{ success() || steps.test.conclusion == 'failure'}} + with: + name: screenshots + path: | + screenshots/* + **/mobile/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected/* + + #- name: Publish Test Report + # # # uses: mikepenz/action-junit-report@v3 + # # # # # TODO: Format a little ? or Use some utility to confier GITHUB_STE_SUMMARY.html below into markdown before outputting it into $GITHUB_STEP_SUMMARY? + # if: ${{ success() || steps.test.conclusion == 'failure'}} + # # # with: + # # # report_paths: '**/build/reports/*Tests/**/*.html' # '**/build/test-results/test/TEST-*.xml' + # run: | + # for file in ./mobile/build/reports/androidTests/connected/*.html; do cat $file >> GITHUB_STEP_SUMMARY.html ; done + # # echo '' >> GITHUB_STEP_SUMMARY.html; + # cat GITHUB_STEP_SUMMARY.html >> $GITHUB_STEP_SUMMARY + release-fastlane: needs: [build] if: startsWith(github.ref, 'refs/tags/v') # only on runs triggered from tag @@ -193,8 +397,8 @@ jobs: path: dist - name: Display structure of downloaded files - run: ls -R working-directory: dist + run: ls -R # detect if version tag is stable/beta - uses: nowsprinting/check-version-format-action@v2 @@ -232,8 +436,8 @@ jobs: path: dist - name: Display structure of downloaded files - run: ls -R working-directory: dist + run: ls -R # detect if version tag is stable/beta - uses: nowsprinting/check-version-format-action@v2 diff --git a/Makefile b/Makefile index a932d55c..ecc34f47 100644 --- a/Makefile +++ b/Makefile @@ -8,13 +8,33 @@ SHELL := /bin/bash RELEASE_TYPE = $(shell $$RELEASE && echo 'release' || echo 'debug') HAS_SECRETS = $(shell test -n "$$JKS_KEYPASS" && echo 'true' || echo 'false') +APKDIR = mobile/build/outputs/apk + # Main targets all: aw-server-rust aw-webui build: all +# builds a complete, signed apk, puts it in dist build-apk: dist/aw-android.apk -dist/aw-android.apk: mobile/build/outputs/apk/release/mobile-release-unsigned.apk +# builds debug and test apks (unsigned) +build-apk-debug: $(APKDIR)/debug/mobile-debug.apk $(APKDIR)/androidTest/debug/mobile-debug-androidTest.apk + mkdir -p dist + cp -r $(APKDIR) dist + +$(APKDIR)/release/mobile-release-unsigned.apk: + TERM=xterm ./gradlew assembleRelease + tree $(APKDIR) + +$(APKDIR)/debug/mobile-debug.apk: + TERM=xterm ./gradlew assembleDebug + tree $(APKDIR) + +$(APKDIR)/androidTest/debug/mobile-debug-androidTest.apk: + TERM=xterm ./gradlew assembleAndroidTest + tree $(APKDIR) + +dist/aw-android.apk: $(APKDIR)/release/mobile-release-unsigned.apk @# TODO: Name the APK based on the version number or commit hash. mkdir -p dist @# Only sign if we have key secrets set ($JKS_KEYPASS and $JKS_STOREPASS) @@ -25,8 +45,10 @@ else ./scripts/sign_apk.sh $< $@ endif -mobile/build/outputs/apk/release/mobile-release-unsigned.apk: - TERM=xterm ./gradlew assembleRelease +# for mobile-debug.apk and mobile-debug-androidTest.apk +dist/debug/%: $(APKDIR)/debug/% + mkdir -p dist + cp $< $@ # aw-server-rust stuff @@ -76,8 +98,17 @@ RUSTFLAGS_ANDROID="-C debuginfo=2 -Awarnings" # This target runs multiple times because it's matched multiple times, not sure how to fix $(RS_SRCDIR)/target/%/$(RELEASE_TYPE)/libaw_server.so: $(RS_SOURCES) echo $@ - echo $(RELEASE_TYPE) - env RUSTFLAGS=$(RUSTFLAGS_ANDROID) make -C aw-server-rust android + echo "Release type: $(RELEASE_TYPE)" + @# if we indicate in CI via USE_PREBUILT that we've + @# fetched prebuilt libaw_server.so from aw-server-rust repo, + @# then don't rebuild it + @# also check libraries exist, if not, error + @if [ $$USE_PREBUILT == "true" ] && [ -f $@ ]; then \ + echo "Using prebuilt libaw_server.so"; \ + else \ + echo "Building libaw_server.so from aw-server-rust repo"; \ + env RUSTFLAGS=$(RUSTFLAGS_ANDROID) make -C aw-server-rust android; \ + fi # aw-webui @@ -104,6 +135,6 @@ clean: test: bundle exec fastlane test - #- ./gradlew clean lint test - #- ./gradlew connectedAndroidTest || true + # ./gradlew clean lint test + # ./gradlew connectedAndroidTest # || true diff --git a/build.gradle b/build.gradle index aa773a3e..056f55d9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,16 +1,20 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.72' + ext.kotlin_version = '1.7.20' + ext.androidXTestVersion = '1.4.0' + ext.espressoVersion = '3.5.0-rc01' + ext.extJUnitVersion = '1.1.3' + ext.servicesVersion = "1.4.2-rc01" repositories { google() - jcenter() + mavenCentral() maven { url "https://plugins.gradle.org/m2/" } } dependencies { - classpath 'com.android.tools.build:gradle:4.1.2' + classpath 'com.android.tools.build:gradle:7.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'gradle.plugin.org.mozilla.rust-android-gradle:plugin:0.8.3' @@ -22,7 +26,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 622ab64a..41dfb879 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-6.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/mobile/build.gradle b/mobile/build.gradle index 80bc16ba..1d95749d 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -3,14 +3,13 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { - compileSdkVersion 29 - buildToolsVersion = '29.0.3' + compileSdkVersion 33 ndkVersion "21.4.7075529" defaultConfig { applicationId "net.activitywatch.android" - minSdkVersion 23 - targetSdkVersion 29 + minSdkVersion 24 + targetSdkVersion 33 // Set in CI on tagged commit versionName "0.10.0" @@ -19,6 +18,7 @@ android { versionCode 25 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments useTestStorageService: 'true' // WARNING: Never commit this uncommented! packagingOptions { @@ -32,13 +32,19 @@ android { } debug { applicationIdSuffix ".debug" + jniDebuggable true + renderscriptDebuggable true } } compileOptions { - sourceCompatibility = "1.8" - targetCompatibility = 1.8 + sourceCompatibility = '1.8' + targetCompatibility = '1.8' } + kotlinOptions { + jvmTarget = "1.8" + } + namespace 'net.activitywatch.android' // Never got this to work... //if (project.hasProperty("doNotStrip")) { // packagingOptions { @@ -57,20 +63,24 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:1.5.0' - implementation 'com.google.android.material:material:1.3.0' + implementation 'com.google.android.material:material:1.7.0' implementation 'com.jakewharton.threetenabp:threetenabp:1.1.1' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + testImplementation "junit:junit:4.13.2" + androidTestImplementation "androidx.test.ext:junit-ktx:$extJUnitVersion" + androidTestImplementation "androidx.test:runner:$androidXTestVersion" + androidTestImplementation "androidx.test:rules:$androidXTestVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" + androidTestUtil "androidx.test.services:test-services:$servicesVersion" + androidTestImplementation "androidx.test.espresso:espresso-web:$espressoVersion" } // Can be used to build with: ./gradlew cargoBuild diff --git a/mobile/src/androidTest/java/net/activitywatch/android/ExampleInstrumentedTest.kt b/mobile/src/androidTest/java/net/activitywatch/android/ExampleInstrumentedTest.kt index 0dc8bb4c..07cce989 100644 --- a/mobile/src/androidTest/java/net/activitywatch/android/ExampleInstrumentedTest.kt +++ b/mobile/src/androidTest/java/net/activitywatch/android/ExampleInstrumentedTest.kt @@ -21,14 +21,14 @@ class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. - val appContext = InstrumentationRegistry.getTargetContext() + val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("net.activitywatch.android", appContext.packageName) } @Test fun getBuckets() { // TODO: Clear test buckets before test - val appContext = InstrumentationRegistry.getTargetContext() + val appContext = InstrumentationRegistry.getInstrumentation().targetContext val ri = RustInterface(appContext) val bucketId = "test-${Math.random()}" val oldLen = ri.getBucketsJSON().length() @@ -38,7 +38,7 @@ class ExampleInstrumentedTest { @Test fun createHeartbeat() { - val appContext = InstrumentationRegistry.getTargetContext() + val appContext = InstrumentationRegistry.getInstrumentation().targetContext val ri = RustInterface(appContext) val bucketId = "test-${Math.random()}" ri.createBucket("""{"id": "$bucketId", "type": "test", "hostname": "test", "client": "test"}""") diff --git a/mobile/src/androidTest/java/net/activitywatch/android/ScreenshotTest.kt b/mobile/src/androidTest/java/net/activitywatch/android/ScreenshotTest.kt new file mode 100644 index 00000000..35f7a004 --- /dev/null +++ b/mobile/src/androidTest/java/net/activitywatch/android/ScreenshotTest.kt @@ -0,0 +1,57 @@ +package net.activitywatch.android + +import android.content.Intent +import android.util.Log +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.core.app.takeScreenshot +import androidx.test.core.graphics.writeToTestStorage +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.rule.GrantPermissionRule +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestName +import java.io.IOException + +private const val TAG = "ScreenshotTest" + +/* + * When this test is executed via gradle managed devices, the saved image files will be stored at + * build/outputs/managed_device_android_test_additional_output/debugAndroidTest/managedDevice/nexusOneApi30/ + */ +class ScreenshotTest { + // a handy JUnit rule that stores the method name, so it can be used to generate unique + // screenshot files per test method + @get:Rule + var permissionRule = GrantPermissionRule.grant(android.Manifest.permission.PACKAGE_USAGE_STATS) + + @get:Rule + var nameRule = TestName() + + private lateinit var scenario: ActivityScenario + + @Test + fun testScreenshot() { + saveDeviceScreenBitmap() + } + + /** + * Captures and saves an image of the entire device screen to storage. + */ + @Throws(IOException::class) + fun saveDeviceScreenBitmap() { + Log.i(TAG, "Running saveDeviceScreenBitmap") + //Thread.sleep(100) + val intent = Intent(ApplicationProvider.getApplicationContext(), MainActivity::class.java) + // TODO: scenarios dont clean up automatically ? + scenario = ActivityScenario.launch(intent) + + // TODO: Not a good method to sleep, need to properly hook on page load + Thread.sleep(5000) + Log.i(TAG, "Taking screenshot") + + val bitmap = takeScreenshot() + bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}") + Log.i(TAG, "Took screenshot!") + } +} diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 0edc3aef..3f1a9176 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> + android:theme="@style/AppTheme.NoActionBar" + android:exported="true"> @@ -48,7 +48,7 @@ + android:exported="true"> @@ -56,7 +56,8 @@ + android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" + android:exported="true"> diff --git a/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt b/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt index 504a9fe9..eaa59381 100644 --- a/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt +++ b/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt @@ -61,7 +61,7 @@ class WebUIFragment : Fragment() { // TODO: Find way to not show the blinking Android error page Log.e(TAG, "WebView received error: $description") arguments?.let { - myWebView.loadUrl(it.getString(ARG_URL)) + it.getString(ARG_URL)?.let { it1 -> myWebView.loadUrl(it1) } } } } @@ -76,7 +76,7 @@ class WebUIFragment : Fragment() { myWebView.settings.javaScriptEnabled = true myWebView.settings.domStorageEnabled = true arguments?.let { - myWebView.loadUrl(it.getString(ARG_URL)) + it.getString(ARG_URL)?.let { it1 -> myWebView.loadUrl(it1) } } return view diff --git a/mobile/src/main/java/net/activitywatch/android/watcher/ChromeWatcher.kt b/mobile/src/main/java/net/activitywatch/android/watcher/ChromeWatcher.kt index fa8452bd..a0b52133 100644 --- a/mobile/src/main/java/net/activitywatch/android/watcher/ChromeWatcher.kt +++ b/mobile/src/main/java/net/activitywatch/android/watcher/ChromeWatcher.kt @@ -58,14 +58,14 @@ class ChromeWatcher : AccessibilityService() { try { if (event != null && event.source != null) { // Get URL - val urlBars = event.source.findAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar") + val urlBars = event.source!!.findAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar") if (urlBars.any()) { val newUrl = "http://" + urlBars[0].text.toString() // TODO: We can't access the URI scheme, so we assume HTTP. onUrl(newUrl) } // Get title - var webView = findWebView(event.source) + var webView = findWebView(event.source!!) if (webView != null) { lastTitle = webView.text.toString() Log.i(TAG, "Title: ${lastTitle}") @@ -73,7 +73,7 @@ class ChromeWatcher : AccessibilityService() { } } catch(ex : Exception) { - Log.e(TAG, ex.message) + Log.e(TAG, ex.message!!) } } diff --git a/mobile/src/main/java/net/activitywatch/android/watcher/UsageStatsWatcher.kt b/mobile/src/main/java/net/activitywatch/android/watcher/UsageStatsWatcher.kt index 8e04fc8f..8ee7a267 100644 --- a/mobile/src/main/java/net/activitywatch/android/watcher/UsageStatsWatcher.kt +++ b/mobile/src/main/java/net/activitywatch/android/watcher/UsageStatsWatcher.kt @@ -1,5 +1,6 @@ package net.activitywatch.android.watcher +import android.Manifest import android.app.AlarmManager import android.app.AppOpsManager import android.app.PendingIntent @@ -9,24 +10,20 @@ import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager -import android.os.AsyncTask -import android.os.Handler -import android.os.Looper -import android.os.SystemClock +import android.os.* +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES import android.provider.Settings import android.util.Log import android.widget.Toast import net.activitywatch.android.RustInterface import net.activitywatch.android.models.Event import org.json.JSONObject -import java.text.SimpleDateFormat import org.threeten.bp.DateTimeUtils import org.threeten.bp.Instant -import java.lang.Thread.sleep import java.net.URL import java.text.ParseException - - +import java.text.SimpleDateFormat class UsageStatsWatcher constructor(val context: Context) { @@ -39,7 +36,27 @@ class UsageStatsWatcher constructor(val context: Context) { var lastUpdated: Instant? = null + // https://stackoverflow.com/a/54839499/4957939 + fun getUsageStatsPermissionsStatus(context: Context): PermissionStatus? { + if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) return PermissionStatus.CANNOT_BE_GRANTED + val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager + val mode = appOps.checkOpNoThrow( + AppOpsManager.OPSTR_GET_USAGE_STATS, + Process.myUid(), + context.packageName + ) + val granted = if (mode == AppOpsManager.MODE_DEFAULT) context.checkCallingOrSelfPermission( + Manifest.permission.PACKAGE_USAGE_STATS + ) == PackageManager.PERMISSION_GRANTED else mode == AppOpsManager.MODE_ALLOWED + return if (granted) PermissionStatus.GRANTED else PermissionStatus.DENIED + } + + enum class PermissionStatus { + GRANTED, DENIED, CANNOT_BE_GRANTED + } + fun isUsageAllowed(): Boolean { + // https://stackoverflow.com/questions/27215013/check-if-my-application-has-usage-access-enabled val applicationInfo: ApplicationInfo = try { context.packageManager.getApplicationInfo(context.packageName, 0) @@ -54,7 +71,8 @@ class UsageStatsWatcher constructor(val context: Context) { applicationInfo.uid, applicationInfo.packageName ) - return mode == AppOpsManager.MODE_ALLOWED + // TODO: Use either of below tests, but the 1st test is not working + return mode == AppOpsManager.MODE_ALLOWED || getUsageStatsPermissionsStatus(context) == PermissionStatus.GRANTED } private fun getUSM(): UsageStatsManager? { @@ -85,7 +103,7 @@ class UsageStatsWatcher constructor(val context: Context) { alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent -> intent.action = "net.activitywatch.android.watcher.LOG_DATA" - PendingIntent.getBroadcast(context, 0, intent, 0) + PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) } val interval = AlarmManager.INTERVAL_HOUR // Or if testing: AlarmManager.INTERVAL_HOUR / 60 diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index bda38f7d..fd66293d 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -21,8 +21,8 @@ Click me to log data! Allows ActivityWatch to read the URL and title from your browser. - - unknown version + Hello blank fragment