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