Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
name: Build & Release

on:
workflow_dispatch:
inputs:
tag:
description: Git tag for the release (leave empty to auto-detect)
required: false
type: string
push:
branches: [main]
paths:
- ".fossify/release-marker.txt"

# Two runs publishing to the same release would race on asset upload
concurrency:
group: build-release-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 60

permissions:
contents: write

steps:
- name: Checkout
uses: actions/checkout@v4
with:
# tags are needed by the "Determine tag" step below; the default shallow
# fetch has none, which silently forces the build-<timestamp> fallback
fetch-depth: 0

- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17

# - name: Set up Android SDK
# uses: android-actions/setup-android@v3
#
# - name: Validate Gradle wrapper
# uses: gradle/actions/wrapper-validation@v4
#
# - name: Setup Gradle
# uses: gradle/actions/setup-gradle@v4

- name: Decode keystore (if provided)
id: keystore
env:
KEYSTORE: ${{ secrets.KEYSTORE }}
run: |
if [ -n "$KEYSTORE" ]; then
echo "$KEYSTORE" | base64 -d > "$RUNNER_TEMP/keystore.jks"
echo "created=true" >> "$GITHUB_OUTPUT"
else
echo "created=false" >> "$GITHUB_OUTPUT"
fi

- name: Build release APKs
env:
SIGNING_KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
# must be absolute: app/build.gradle.kts resolves file() relative to the app module
SIGNING_STORE_FILE: ${{ steps.keystore.outputs.created == 'true' && format('{0}/keystore.jks', runner.temp) || '' }}
SIGNING_STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
run: |
if [ -z "$SIGNING_STORE_FILE" ]; then
unset SIGNING_KEY_ALIAS SIGNING_KEY_PASSWORD SIGNING_STORE_FILE SIGNING_STORE_PASSWORD
fi
# `assemble` would also build the three debug variants, which are never published
./gradlew assembleRelease

- name: Collect APKs
id: apks
run: |
mkdir -p apks
find app/build/outputs/apk -name '*.apk' -type f -exec cp {} apks/ \;
ls -lh apks/
echo "count=$(ls apks/*.apk 2>/dev/null | wc -l)" >> "$GITHUB_OUTPUT"

- name: Determine tag
id: tag
env:
INPUT_TAG: ${{ inputs.tag }}
run: |
if [ -n "$INPUT_TAG" ]; then
echo "value=$INPUT_TAG" >> "$GITHUB_OUTPUT"
exit 0
fi
DESCRIBE=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$DESCRIBE" ]; then
echo "value=build-$(date +%Y%m%d%H%M%S)" >> "$GITHUB_OUTPUT"
else
echo "value=$DESCRIBE" >> "$GITHUB_OUTPUT"
fi

- name: Upload APKs as workflow artifact
uses: actions/upload-artifact@v4
with:
name: phone-apks-${{ steps.tag.outputs.value }}
path: apks/*.apk
if-no-files-found: error

# Unsigned APKs cannot be installed as an update over a signed install, so they stay
# as a workflow artifact only and never reach a release.
- name: Create or update GitHub release
if: steps.apks.outputs.count > 0 && steps.keystore.outputs.created == 'true'
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.tag.outputs.value }}
run: |
echo "Tag: $TAG"

if gh release view "$TAG" --repo "${{ github.repository }}" &>/dev/null; then
echo "Release $TAG exists — uploading assets"
gh release upload "$TAG" apks/*.apk --clobber --repo "${{ github.repository }}"
else
echo "Release $TAG does not exist — creating"
gh release create "$TAG" apks/*.apk \
--title "$TAG" \
--notes "Automated build for $TAG" \
--repo "${{ github.repository }}"
fi
128 changes: 128 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
SHELL := /bin/sh
.DEFAULT_GOAL := help

# Which product flavor to build. The project declares core/foss/gplay; the bare
# assemble<BuildType>/install<BuildType> tasks either build all three flavors at once
# (assembleDebug) or do not exist at all (installDebug), so every target below is
# flavor-qualified. Override with e.g. `make FLAVOR=core debug`.
FLAVOR ?= foss

# Gradle wrapper
GRADLE := ./gradlew

# JDK discovery, in order of preference:
# 1. JAVA_HOME from the environment, if it already points at a usable JDK
# 2. macOS: java_home, which knows about /Library/Java/JavaVirtualMachines
# 3. Linux/WSL: probe /usr/lib/jvm, picking the first JDK new enough
# 4. whatever javac is on PATH
# AGP needs JDK 17 or newer; 17, 21 and 25 all build this project. Source and target
# compatibility stay pinned in gradle/libs.versions.toml, so the bytecode does not
# depend on which of them you use.
MIN_JDK := 17

# $(call jdk_major,<home>) -> major version, or empty if that is not a JDK at all.
# The -x test keeps the shell from printing "not found" straight to the terminal.
jdk_major = $(shell [ -x "$(1)/bin/javac" ] && "$(1)/bin/javac" -version 2>&1 | sed -e 's/^javac //' -e 's/[.+_-].*//')
jdk_ok = $(shell [ "$(call jdk_major,$(1))" -ge $(MIN_JDK) ] 2>/dev/null && echo ok)

# `make help` has to work before a JDK is installed, so skip discovery entirely for it.
JDK_NEEDED := $(filter-out help,$(or $(strip $(MAKECMDGOALS)),$(.DEFAULT_GOAL)))

ifneq ($(strip $(JDK_NEEDED)),)

# An inherited JAVA_HOME that is too old is common (distro default, old shell profile).
# Warn and keep looking rather than failing, since a newer JDK is usually installed too.
ifneq ($(strip $(JAVA_HOME)),)
ifneq ($(call jdk_ok,$(JAVA_HOME)),ok)
$(warning ignoring JAVA_HOME=$(JAVA_HOME): not a JDK $(MIN_JDK)+)
JAVA_HOME :=
endif
endif

# Darwin / Linux / MINGW*_NT / CYGWIN*_NT. Everything non-Darwin tries the Linux layout
# first and then falls back to PATH, which is what MSYS and Cygwin need anyway.
UNAME_S := $(shell uname -s)

ifeq ($(strip $(JAVA_HOME)),)
ifeq ($(UNAME_S),Darwin)
JAVA_HOME := $(shell /usr/libexec/java_home -v $(MIN_JDK)+ 2>/dev/null)
else
# do not sort these lexically: java-8-openjdk sorts after java-21-openjdk, so ask
# each candidate for its version instead of guessing from the directory name
JAVA_HOME := $(shell \
for d in /usr/lib/jvm/*/; do \
[ -x "$$d/bin/javac" ] || continue; \
v=$$("$$d/bin/javac" -version 2>&1 | sed -e 's/^javac //' -e 's/[.+_-].*//'); \
if [ "$$v" -ge $(MIN_JDK) ] 2>/dev/null; then printf '%s\n' "$${d%/}"; fi; \
done | head -1)
endif
endif

ifeq ($(strip $(JAVA_HOME)),)
JAVA_HOME := $(patsubst %/bin/javac,%,$(realpath $(shell command -v javac 2>/dev/null)))
endif

ifeq ($(strip $(JAVA_HOME)),)
$(error No JDK $(MIN_JDK)+ found. Install one (macOS: brew install --cask temurin@$(MIN_JDK), \
Debian/Ubuntu: apt install openjdk-$(MIN_JDK)-jdk, Fedora: dnf install java-$(MIN_JDK)-openjdk-devel) \
or set JAVA_HOME yourself)
endif

JDK_MAJOR := $(call jdk_major,$(JAVA_HOME))
ifneq ($(call jdk_ok,$(JAVA_HOME)),ok)
$(error Found JDK "$(JDK_MAJOR)" at $(JAVA_HOME), but $(MIN_JDK)+ is required. \
Set JAVA_HOME to a newer JDK)
endif

export JAVA_HOME

# so gradlew and anything it spawns pick up the same java
PATH := $(JAVA_HOME)/bin:$(PATH)
export PATH

endif

.PHONY: help all debug release clean check install install-debug install-release java-info

# Lists every target carrying a `## description` comment. Deliberately plain POSIX awk:
# no lazy quantifiers (BSD and GNU disagree) and no sed \t (BSD sed does not expand it).
help: ## Show this help
@echo 'Targets:'
@awk 'BEGIN {FS = ":.*?## "} { \
if (/^[0-9a-zA-Z_-]+:.*?##.*$$/) {printf " \033[1;37m%-20s\033[1;32m%s\033[0m\n", $$1, $$2} \
else if (/^## .*$$/) {printf " \033[1;36m%s\033[0m\n", substr($$1,4)} \
}' $(MAKEFILE_LIST)
@echo ""
@echo " FLAVOR=core|foss|gplay selects the product flavor (currently $(FLAVOR))."
@echo " Example: make FLAVOR=core install-debug"

all: release install-release ## release + install-release

debug: ## Assemble the debug APK
@$(GRADLE) assemble$(FLAVOR)Debug

release: ## Assemble the release APK
@$(GRADLE) assemble$(FLAVOR)Release

install: install-debug ## Alias for install-debug

# install<Variant> already depends on assemble<Variant>, no need to chain it ourselves
install-debug: ## Build and install the debug APK on the connected device
@$(GRADLE) install$(FLAVOR)Debug

install-release: ## Build and install the release APK on the connected device
@$(GRADLE) install$(FLAVOR)Release

clean: ## Delete build outputs
@$(GRADLE) clean

# Static analysis. The project has no formatter configured (no spotless/ktlint plugin),
# only detekt and the Android linter.
check: ## Run detekt and the Android linter
@$(GRADLE) detekt lint$(FLAVOR)Debug

# handy when a build picks up an unexpected toolchain
java-info: ## Print the detected JDK and flavor
@echo "JAVA_HOME = $(JAVA_HOME)"
@echo "JDK major = $(JDK_MAJOR)"
@echo "flavor = $(FLAVOR)"
12 changes: 11 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
Expand Down Expand Up @@ -99,6 +100,13 @@
android:label="@string/conference"
android:parentActivityName="org.fossify.phone.activities.CallActivity" />

<activity
android:name=".activities.PermissionsActivity"
android:configChanges="orientation"
android:exported="false"
android:label="@string/permissions_and_access"
android:parentActivityName=".activities.MainActivity" />

<activity
android:name=".activities.SettingsActivity"
android:configChanges="orientation"
Expand All @@ -119,7 +127,8 @@
android:label="@string/ongoing_call"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:showOnLockScreen="true" />
android:showOnLockScreen="true"
android:turnScreenOn="true" />

<activity
android:name=".activities.DialpadActivity"
Expand Down Expand Up @@ -172,6 +181,7 @@
android:name=".services.CallService"
android:enabled="true"
android:exported="true"
android:foregroundServiceType="phoneCall"
android:permission="android.permission.BIND_INCALL_SERVICE">
<meta-data
android:name="android.telecom.IN_CALL_SERVICE_UI"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,10 @@ class CallActivity : SimpleActivity() {

@SuppressLint("NewApi")
private fun addLockScreenFlags() {
// FLAG_KEEP_SCREEN_ON is not covered by setShowWhenLocked/setTurnScreenOn, without it the
// screen times out while the call UI is up
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

if (isOreoMr1Plus()) {
setShowWhenLocked(true)
setTurnScreenOn(true)
Expand All @@ -837,7 +841,6 @@ class CallActivity : SimpleActivity() {
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
)
}

Expand Down
Loading
Loading