blob: 1ab67ea34b56f64cb8013aa19bc97e75201957b8 [file] [log] [blame]
#!/bin/sh
# Copyright 2018 Fairphone B.V.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
set -o errexit
set -o nounset
# Print the extended trace if we are verbose. Do not disable the trace if we are
# not verbose to keep what the environment defines.
if [ "${VERBOSE:-false}" = true ]; then
set -o xtrace
fi
usage() {
cat <<heredoc
Usage: $0 [-h] [-s SERIAL] OTA_FILE
Copy an OTA update file to a device
Options:
-h, --help Print this help message.
-r, --click_reboot Automatically hit the reboot button for starting the
installation of the OTA package. This needs a Python 3
environment with uiautomator installed.
Run \`pip3 install uiautomator\` to install it in your
(virtual) environment.
-s, --serial Specify a target device based on its adb serial number.
Globals:
VERBOSE Set to "true" to enable xtrace (shell debug output).
Otherwise, the xtrace option will be left as it is.
heredoc
}
SERIAL=
ADB_ARGUMENTS=
LOG_PREFIX=
TARGET_DIR=/mnt/sdcard/Download
CLICK_REBOOT=false
UPDATER_ACTIVITY=
log() {
echo "${LOG_PREFIX}$*"
}
kill_updater() {
adb ${ADB_ARGUMENTS} shell am force-stop com.fairphone.updater
}
click_reboot() {
# shellcheck disable=SC2039
local serial_string=""
if [ -n "${SERIAL}" ]; then
serial_string="\"${SERIAL}\""
fi
python3 -c \
"from uiautomator import Device
dut = Device(${serial_string})
button = dut(resourceId=\"com.fairphone.updater:id/restart_button\")
button.click()"
}
trigger_update() {
# Make sure we start off with a fresh state, especially in cases of app crashes.
log "Killing the Updater if it is currently running…"
kill_updater || true
# Ensure the screen is on and unlocked. The first call will activate the
# screen if it is off, the second will unlock it. These calls have no effect
# if the screen is active already.
adb ${ADB_ARGUMENTS} shell input keyevent KEYCODE_MENU
adb ${ADB_ARGUMENTS} shell input keyevent KEYCODE_MENU
log "Starting the Updater activity…"
adb ${ADB_ARGUMENTS} shell am start -a android.intent.action.VIEW \
-t application/zip \
-c android.intent.category.DEFAULT \
-d "file:///${TARGET_DIR}/$(basename "${OTA_UPDATE_FILE}")" \
"${UPDATER_ACTIVITY}"
if [ "${CLICK_REBOOT}" = true ]; then
log "Triggering reboot for installing the OTA…"
# Trigger UI automation. This may fail due to a bug in uiautomator:
# https://github.com/xiaocong/uiautomator/issues/210
# Retrying a couple of times usually fixes the issue here.
# shellcheck disable=SC2039
local ui_max_tries=5
# shellcheck disable=SC2039
local ui_retry_wait_time=20s
# shellcheck disable=SC2039
local i
for i in $(seq "${ui_max_tries}"); do
adb ${ADB_ARGUMENTS} wait-for-device
if click_reboot; then
break;
fi
if [ "${i}" -eq "${ui_max_tries}" ]; then
log "click_reboot failed too often. Aborting."
return 1
fi
log "click_reboot: Try $i failed. \
Trying again in ${ui_retry_wait_time}."
sleep "${ui_retry_wait_time}"
done
fi
}
# Allow to automatically retry occasionally failing commands.
# This is a simplistic workaround for various kinds of failures. The number of
# retries and wait time in between is therefore fairly arbitrary and result of
# experimentation.
retry_cmd() {
# shellcheck disable=SC2039
local max_tries=5
# shellcheck disable=SC2039
local i
for i in $(seq "${max_tries}"); do
# shellcheck disable=SC2039
local ret_val=0
"$@" || ret_val=$?
if [ "${ret_val}" -eq 0 ]; then
return 0
fi
log "Following command failed: $*"
if [ "$i" -eq "${max_tries}" ]; then
log "Too many failures (${max_tries}), aborting."
return "${ret_val}"
fi
log "Try $i of ${max_tries} failed. Trying again."
sleep 10s
done
return "${ret_val}"
}
while [ $# -gt 0 ]; do
case "$1" in
-r|--click_reboot)
CLICK_REBOOT=true
shift
;;
-s|--serial)
if [ $# -lt 2 ]; then
echo "Missing device serial number" >&2
usage >&2
exit 1
fi
SERIAL="$2"
ADB_ARGUMENTS="${ADB_ARGUMENTS} -s ${SERIAL}"
LOG_PREFIX="[$2] "
shift 2
;;
-h|--help)
usage
exit 0
;;
-*)
echo "Unknown option $1" >&2
usage >&2
exit 1
;;
*)
if [ -n "${OTA_UPDATE_FILE:-}" ]; then
echo "Expecting exactly one positional argument (OTA_FILE)." >&2
echo "Unexpected argument is \"$1\"" >&2
usage >&2
exit 1
fi
OTA_UPDATE_FILE="$1"
shift
;;
esac
done
if [ -z "${OTA_UPDATE_FILE}" ]; then
echo "Missing OTA update file." >&2
usage >&2
exit 1
fi
if [ ! -f "${OTA_UPDATE_FILE}" ]; then
echo "${OTA_UPDATE_FILE} not found" >&2
exit 1
fi
log "Waiting for device to be available…"
adb ${ADB_ARGUMENTS} wait-for-device
ANDROID_API_LEVEL="$(adb ${ADB_ARGUMENTS} shell getprop ro.build.version.sdk)"
log "Pushing ${OTA_UPDATE_FILE} to device…"
# `adb push` occasionally fails for unclear reasons. Usually this can be fixed
# by running the same command again.
retry_cmd adb ${ADB_ARGUMENTS} push "${OTA_UPDATE_FILE}" "${TARGET_DIR}/"
if [ "${ANDROID_API_LEVEL}" -ge 23 ]; then
# Android 6 introduces post-install/run-time permissions.
log "Granting storage read permission to the Updater…"
adb ${ADB_ARGUMENTS} shell pm grant com.fairphone.updater \
android.permission.READ_EXTERNAL_STORAGE
fi
# The activity was renamed from FairphoneUpdater to UpdaterActivity in v1.39.9
UPDATER_ACTIVITY=$(adb ${ADB_ARGUMENTS} shell dumpsys package \
com.fairphone.updater \
| dos2unix \
| grep -A 2 "Base MIME Types:" \
| sed -n -e 's/^.*\(com.fairphone.updater\/.*\)$/\1/p' \
| awk '{print $1;}')
# Trigger the Updater activity and also reboot, if requested.
# In specific cases and Updater versions, the Updater may crash here. Try working around that by
# trying again after a fresh start of the app.
retry_cmd trigger_update