1#!/bin/bash
2set -e
3set -m
4
5# This is a wrapper script that runs the specific version of Android Studio that is recommended for developing in this repository.
6# (This serves a similar purpose to gradlew)
7
8# Get the property value by a properties file
9function getProperyValue() {
10  getProperyValueAbs ${scriptDir}/$1 $2
11}
12
13function getProperyValueAbs() {
14  # Use --no-messages to suppress error messages about non-existant files
15  echo "$(grep --no-messages "$2[[:space:]]*=[[:space:]]*" ${1} | sed 's/[^=]*=[[:space:]]*//')"
16}
17
18# Get the studio version corresponding to the gradle version defined in gradle.properties
19function fetchStudioUrl() {
20  local cache_file=.studio_version_cache
21  local studioVersion="$(echo "$(getProperyValue ManagedProvisioningGradleProject/gradle/libs.versions.toml android-studio)" | sed 's/"//g')"
22  local cachedVersion="$(getProperyValue $cache_file cached_studio_version)"
23
24  if [ ! "$studioVersion" == "$cachedVersion" ]; then
25    local content="$(curl -L https://developer.android.com/studio/archive.html)"
26    local iframe_url="$(echo $content | egrep -o 'iframe src="[^"]+"' | cut -d '"' -f 2)"
27    content="$(curl -L $iframe_url)"
28
29    if [ "$osName" == "mac" ]; then
30      content="$(echo $content | egrep -o "Android Studio [^0-9]+$studioVersion.+?section")"
31    else
32      content="$(echo $content | grep -Po "Android Studio [^0-9]+$studioVersion.+?section")"
33    fi
34
35    local mac_url
36    if [ "$arch" == "arm" ]; then
37      mac_url="$(echo $content | egrep -o '"[^"]+mac_arm.zip"' | cut -d '"' -f 2)"
38    else
39      mac_url="$(echo $content | egrep -o '"[^"]+mac.zip"' | cut -d '"' -f 2)"
40    fi
41    mac_url="$(echo $mac_url | cut -d " " -f 1)"
42
43    local linux_url="$(echo $content | egrep -o '"[^"]+linux[^"]*"' | cut -d '"' -f 2 | cut -d " " -f 1)"
44    linux_url="$(echo $linux_url | cut -d " " -f 1)"
45
46    echo cached_studio_version=$studioVersion > ${scriptDir}/$cache_file
47    echo mac=$mac_url >> ${scriptDir}/$cache_file
48    echo linux=$linux_url >> ${scriptDir}/$cache_file
49  fi
50  studioUrl="https://dl.google.com/dl/android/studio/$(getProperyValue $cache_file $osName | egrep -o  'ide-zips/.*')"
51}
52
53# Escape sequence to print bold colors
54RED='\033[1;31m'
55GREEN='\033[1;32m'
56YELLOW='\033[1;33m'
57# Escape sequence to clear formatting
58NC='\033[0m'
59
60printError() {
61  local logMessage="$1"
62  echo -e "${RED}ERROR:${NC} ${logMessage}"
63}
64
65printWarning() {
66  local logMessage="$1"
67  echo -e "${YELLOW}WARNING:${NC} ${logMessage}"
68}
69
70printInfo() {
71  local logMessage="$1"
72  echo -e "${GREEN}INFO:${NC} ${logMessage}"
73}
74
75
76acceptsLicenseAgreement="false"
77runStudio="true"
78downloadStudioZip="true"
79cleanProjectFiles="false"
80scriptDir="$(cd $(dirname $0) && pwd)"
81projectDir=$scriptDir/ManagedProvisioningGradleProject
82androidBuildTop="$(cd "${scriptDir}/../../../../"; pwd)"
83
84gradleOutDir="${androidBuildTop}/out/gradle"
85usingDefaultSettingsDir='true'
86androidStudioSettingsDir="${gradleOutDir}/AndroidStudio"
87# Where to put the generated idea.properties file relative to $androidStudioSettingsDir, used for
88# STUDIO_PROPERTIES
89ideaPropRelPath="bin/idea.properties"
90
91# TODO(b/249826650): Maybe we shouldn't write to ~/.AndroidStudio* named directories. Android Studio
92# searches for these directories and tries to use them when opening for the first time elsewhere.
93# See:
94#  - Implementation details: http://shortn/_Q0gp64FPj3
95#  - Screenshot: http://screen/4sCQBhNyVTMPPf3.png
96studioHomeDir="${HOME}/.AndroidStudioSystemUI"
97studioSetupDir="${studioHomeDir}/bin"
98function getOsName() {
99  local unameOutput="$(uname)"
100  local osName=""
101  if [ "${unameOutput}" == "Linux" ]; then
102    osName="linux"
103  else
104    osName="mac"
105  fi
106  echo "${osName}"
107}
108osName="$(getOsName)"
109arch="$(uname -p)"
110studioUrl=
111# If empty string, don't update the SDK. Otherwise, the string indicates the method used for
112# fetching the artifacts needed to generate the SDK. Currently, either 'adb' or 'soong'
113updateSdk=''
114copySdkSourceDir=''
115
116function setupBuildSrcSymlinks() {
117  # Builtbots can't write to the source dirs, and there is no gradle option to overwrite the .gradle
118  # location of buildSrc, so we use symlinks. The dirs must be created before running the build.
119  #
120  # Alternatively, we could migrate from buildSrc/ to a composite build to avoid needing the
121  # symlinks. See: http://go/gh/gradle/gradle/issues/2472#issuecomment-315376378
122  cd "${scriptDir}/ManagedProvisioningGradleProject/buildSrc"
123  mkdir -p $(readlink .gradle)
124  mkdir -p $(readlink build)
125  cd - > /dev/null
126}
127
128# Used to keep track of whether we confirmed adb root works. We only want to check once.
129adbChecked='false'
130function assertHasAdbRoot() {
131  if [[ "${adbChecked}" == 'false' && "${updateSdk}" == 'adb' ]]; then
132    adb root || {
133      printError 'adb root failed. You must have a rooted device attached to use "--update-sdk adb".'
134      if [ "${osName}" != "mac" ]; then
135        echo 'NOTE: On Linux, you can use "--update-sdk soong" to compile the SDK from source.'
136      fi
137      exit 1
138    }
139    adb wait-for-device
140    adbChecked='true'
141  fi
142}
143
144function getUpstreamGitBranch() {
145  echo "$(git -C "${androidBuildTop}/.repo/manifests" for-each-ref --format='%(upstream:short)' refs/heads | sed 's/origin\///')"
146}
147function toUpperCase() {
148  local dashedName="$1"
149  IFS='-'
150  local words=($dashedName)
151  unset IFS
152  local upperCaseName=''
153  for word in "${words[@]}"; do
154    upperCaseName="${upperCaseName}$(echo "$word" | sed 's/[a-z]/\U&/')"
155  done
156  echo "$upperCaseName"
157}
158# Find the name of the branch (e.g. udc-dev) and change it to camel-case (e.g. UdcDev)
159# (e.g. udc-dev)
160branchName="$(getUpstreamGitBranch)"
161# (e.g. UdcDev)
162upperCaseBranchName="$(toUpperCase "$branchName")"
163
164sdkVersionInt="$(getProperyValue ManagedProvisioningGradleProject/gradle.properties TARGET_SDK)"
165# Use the first 4 chars of the SHA of $ANDROID_BUILD_TOP as a unique ID for this Android tree.
166# We will use this to associate this tree with the generated SDK so that all platform SDK can share
167# the same SDK home. This is just for preventing collisions if someone has the same branch checked
168# out twice. Otherwise, we'd just use $upperCaseBranchName
169androidTreeUniqueId="$(echo "$androidBuildTop" | shasum -a 256 | head -c 4)"
170
171# ---- ---- ---- ----
172# The following variables are to correspond with the Android Gradle DSL property of the same name:
173# Public docs: http://go/android-reference/tools/gradle-api/8.0/com/android/build/api/dsl/SettingsExtension#compileSdkPreview()
174# Parsing code: http://go/aocs/android/platform/superproject/+/studio-main:tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dsl/CommonExtensionImpl.kt
175compileSdkPreview="${upperCaseBranchName}ForTree${androidTreeUniqueId}"
176compileSdkVersion="android-${compileSdkPreview}"
177# ---- ---- ---- ----
178
179prebuiltsDir="${androidBuildTop}/prebuilts"
180function getLegacySdkDir() {
181  if [ "${osName}" == "mac" ]; then
182    echo "${prebuiltsDir}/fullsdk-darwin"
183  else
184    echo "${prebuiltsDir}/fullsdk-linux"
185  fi
186}
187function getLegacySdkDir2() {
188  if [ "${osName}" == "mac" ]; then
189    echo "${studioHomeDir}/fullsdk-darwin"
190  else
191    echo "${studioHomeDir}/fullsdk-linux"
192  fi
193}
194function getSdkDir() {
195  echo "${gradleOutDir}/MockSdk"
196}
197sdkDir="$(getSdkDir)"
198platformDir="$sdkDir/platforms/$compileSdkVersion"
199
200function printHelpAndExit() {
201  local message=(
202    'Usage: studiow [OPTION...]'
203    '\n'
204    '\n  -y, --accept-license-agreement'
205    '\n  --update-only'
206    '\n  --no-download'
207    '\n  --update-sdk [ARTIFACT SOURCE (adb, soong, or copy)]'
208    '\n  --project-dir'
209    '\n  --settings-dir'
210    '\n  --clean'
211    '\n  --uninstall'
212  )
213  local fmt_cmd=(fmt -w 100 --split-only)
214  if [ "$osName" == "mac" ]; then
215    fmt_cmd=(fmt -w 100)
216  fi
217  printf "$(printf %s "${message[@]}")" | ${fmt_cmd[@]}
218  exit 1
219}
220
221
222function parseOptions() {
223  while :; do
224    case "$1" in
225      -y|--accept-license-agreement)
226        acceptsLicenseAgreement="true"
227        ;;
228      --update-only)
229        runStudio="false"
230        ;;
231      --no-download)
232        downloadStudioZip="false"
233        runStudio="false"
234        ;;
235      --update-sdk)
236        if [[ -n "$2" && "$2" != -* ]]; then
237          updateSdk="$2"
238          shift
239        fi
240        case "$updateSdk" in
241          adb)
242            ;;
243          soong)
244            if [[ "${osName}" == "mac" ]]; then
245              printError 'MacOS does not support soong builds'
246              exit 1
247            elif [[ ! -f "$androidBuildTop/build/soong/soong_ui.bash" ]]; then
248              printError 'You must have a full platform branch to compile the SDK. Minimal checkouts (e.g. *-sysui-studio-dev) do not support soong builds.'
249              exit 1
250            fi
251            ;;
252          copy)
253            if [[ -n "$2" && "$2" != -* ]]; then
254              copySdkSourceDir="$2"
255              shift
256            else
257              printError 'You must specify a directoy to copy the SDK from'
258              echo ''
259              echo 'Usage: ./studiow --update-sdk copy [DIR]'
260              echo ''
261              echo 'Directory can be local or remote in the form of [user@]host:[path].'
262              echo ''
263              echo 'For example, to copy the SDK from a remote host, run the following:'
264              echo ''
265              echo '  ./studiow --update-sdk copy example.corp.google.com:/path/to/out/gradle/MockSdk/platforms/android-31'
266              echo ''
267              exit 1
268            fi
269            ;;
270          *)
271            if [[ -z "${updateSdk}" ]]; then
272              printError 'You must specify artifact source when using --update-sdk.'
273            else
274              printError "Unknown SDK artifact source: $updateSdk"
275            fi
276            echo ''
277            echo 'Usage: ./studiow --update-sdk [ARTIFACT SOURCE]'
278            echo ''
279            echo 'Available options are:'
280            echo '  adb: Pull the artifacts from attached device (default)'
281            echo '  soong: Build the artifacts using soong (recommended)'
282            echo '  copy: Copy android.jar and framework.aidl from the given directory'
283            echo ''
284            echo 'NOTE: soong option can only be used on Linux with a full checkout'
285            exit 1
286            ;;
287        esac
288        ;;
289      --pull-sdk)
290        printWarning "--pull-sdk is deprecated. It is equivalent to '--update-sdk adb'"
291        updateSdk='adb'
292        ;;
293      --project-dir)
294        shift
295        projectDir="$1"
296        ;;
297      --settings-dir)
298        shift
299        usingDefaultSettingsDir='false'
300        if [[ -z "$1" ]] ; then
301          echo "--settings-dir expects a directory. Usage: --settings-dir [dir name]"
302          exit 1
303        elif [[ ! -d "$1" ]] ; then
304          echo "Invalid --settings-dir: $1 does not exist or is not a directory"
305          exit 1
306        fi
307        androidStudioSettingsDir="$(cd "$1"; pwd)"
308        ;;
309      --clean)
310        cleanProjectFiles="true"
311        ;;
312      --uninstall)
313        uninstallAndroidStudio="true"
314        ;;
315      -h|--help|-?)
316        printHelpAndExit
317        ;;
318      *)
319        if [ -z "$1" ]; then
320          # If $1 is an empty string, it means we reached the end of the passed arguments
321          break
322        else
323          echo "Unknown option: $1"
324          exit
325        fi
326    esac
327
328    shift
329  done
330}
331
332# $1 - string to print
333# $2 - default, either 'y' or 'n'
334function yesNo() {
335  local question="$1"
336  local defaultResponse="${2:-y}"
337  local yesNoPrompt=''
338
339  if [[ "${defaultResponse::1}" =~ [yY] ]]; then
340    yesNoPrompt='[Y/n]'
341  else
342    yesNoPrompt='[y/N]'
343  fi
344
345  read -r -n 1 -p "$question ${yesNoPrompt}? " -s reply
346  if [ -z "${reply}" ]; then
347    # Replace the empty string with the default response
348    reply="${defaultResponse::1}"
349  fi
350
351  # Print the response so there is no confusion
352  echo "${reply::1}"
353
354  case "${reply::1}" in
355    [yY])
356      true
357      ;;
358    *)
359      false
360      ;;
361  esac
362}
363
364function downloadFile() {
365  fromUrl="$1"
366  destPath="$2"
367  tempPath="${destPath}.tmp"
368  if [ -f "${destPath}" ]; then
369    if yesNo "File already exists. Do you want to delete and re-download?"; then
370      rm "${destPath}"
371    fi
372  fi
373
374  if [ -f "${destPath}" ]; then
375    echo "Using existing file from ${destPath}"
376  else
377    echo "Downloading ${fromUrl} to ${destPath}"
378    curl "${fromUrl}" > "${tempPath}"
379    mv "${tempPath}" "${destPath}"
380  fi
381}
382
383function findStudioMacAppPath() {
384  echo "$(find "${studioUnzippedPath}" -type d -depth 1 -name "Android Studio*.app")"
385}
386
387function getLicensePath() {
388  if [ "${osName}" == "mac" ]; then
389    appPath="$(findStudioMacAppPath)"
390    echo "${appPath}/Contents/Resources/LICENSE.txt"
391  else
392    echo "${studioUnzippedPath}/android-studio/LICENSE.txt"
393  fi
394}
395
396function checkLicenseAgreement() {
397  # TODO: Is there a more official way to check that the user accepts the license?
398
399  licenseAcceptedPath="${studioUnzippedPath}/STUDIOW_LICENSE_ACCEPTED"
400
401  if [ ! -f "${licenseAcceptedPath}" ]; then
402    if [ "${acceptsLicenseAgreement}" == "true" ]; then
403      touch "${licenseAcceptedPath}"
404    else
405      if yesNo "Do you accept the license agreement at $(getLicensePath)"; then
406        touch "${licenseAcceptedPath}"
407      else
408        exit 1
409      fi
410    fi
411  fi
412}
413
414# Inserts a snippet into the studio.sh launch script that prevents it from running if
415# STUDIO_LAUNCHED_WITH_WRAPPER is not set. This is to prevent people from launching studio.sh
416# directly, which can result in build errors. This only works on Linux. Unfortunately, there is no
417# equivalent for Mac OS.
418#
419# Inputs:
420#   osName - Either mac or linux
421#   studioPath - Path to the studio.sh script on Linux
422function updateStudioLaunchScript() {
423  # Only Linux uses the studio.sh script
424  if [ "${osName}" == "mac" ]; then
425    return
426  fi
427  # If studio.sh already contains 'STUDIO_LAUNCHED_WITH_WRAPPER', don't do anything
428  grep -qF 'STUDIO_LAUNCHED_WITH_WRAPPER' "$studioPath" && return
429
430  local tmpStudioSh="$(mktemp)"
431
432  # Find the first line OS_TYPE, and use this as our insertion point.
433  local insertionPoint="$(grep --line-number OS_TYPE "$studioPath" | head -n 1 | cut -d ':' -f 1)"
434
435  # Dump everything from line 0 through $insertionPoint-1 into the tmp file
436  head -n "$(("$insertionPoint"-1))" "$studioPath" > "$tmpStudioSh"
437
438  # Insert a conditional to prevent launching studiow when STUDIO_LAUNCHED_WITH_WRAPPER is unset
439  ( cat <<'EOF'
440if [ -z "$STUDIO_LAUNCHED_WITH_WRAPPER" ]; then
441  message 'This installation of Android Studio must be launched using the studiow script found in $ANDROID_BUILD_TOP/packages/apps/ManagedProvisioning/studio-dev/studiow.'
442  exit 1
443fi
444EOF
445) >> "$tmpStudioSh"
446
447  # Dump everything from $insertionPoint until the end studio.sh into the tmp file
448  tail -n +"$insertionPoint" "$studioPath" >> "$tmpStudioSh"
449
450  # Ensure that the tmp file has the same permissions as the original studio.sh
451  chmod --reference="$studioPath" "$tmpStudioSh"
452
453  echo "Inserting the following snippet into studio.sh to prevent launching it outside of studiow:"
454  diff "$studioPath" "$tmpStudioSh" || :
455  mv "$tmpStudioSh" "$studioPath"
456}
457
458# Creates an idea.properties file where all the configs are specific to this checkout of the Android
459# tree and not shared between different branches.
460#
461# Inputs:
462#  scriptDir - The path to this script (studiow)
463#  androidStudioSettingsDir - The common dir for Android Studio settings per checkout
464#  studioPropertiesFile - Where to put the generated idea.properties file, used for STUDIO_PROPERTIES
465function updateIdeaProperties() {
466  local ideaPropRefFile="${scriptDir}/development/studio/idea.properties"
467  mkdir -p "$(dirname "$studioPropertiesFile")"
468  echo "SYSUI_STUDIO_SETTINGS_DIR=$androidStudioSettingsDir" > "$studioPropertiesFile"
469  chmod 640 "$studioPropertiesFile"
470  cat "${ideaPropRefFile}" >> "$studioPropertiesFile"
471}
472
473# Creates the DO_NOT_RUN_ANDROID_STUDIO_FROM_HERE file
474#
475# Inputs:
476#   studioSetupDir - Path to Android Studio bin dir. Assumes the dir already exists.
477function createWarningTextFile() {
478  ( cat <<'EOF'
479The installations of Android Studio found in this directory MUST be launched using the studiow
480script found in:
481$ANDROID_BUILD_TOP/packages/apps/ManagedProvisioning/studio-dev/studiow
482
483Do NOT launch Android Studio by running studio.sh (on Linux) or by opening Android Studio.app (on
484MacOS). Otherwise, you won't be able to work on multiple branches of Android simultaneously.
485EOF
486) > "${studioSetupDir}/DO_NOT_RUN_ANDROID_STUDIO_FROM_HERE"
487}
488
489function updateStudio() {
490
491  # skip if already up-to-date
492  if stat "${studioUnzippedPath}" >/dev/null 2>/dev/null; then
493    # already up-to-date
494    createWarningTextFile
495    return
496  fi
497
498  mkdir -p "${studioSetupDir}"
499  downloadFile "${studioUrl}" "${studioZipPath}"
500  echo
501
502  echo "Unzipping"
503  if [[ $studioZipPath = *.zip ]]; then
504    unzip "${studioZipPath}" -d "${studioUnzippedPath}"
505  elif [[ $studioZipPath = *.tar.gz ]]; then
506    mkdir -p $studioUnzippedPath
507    tar -xf $studioZipPath -C $studioUnzippedPath
508  fi
509
510  createWarningTextFile
511}
512
513# ANDROID_LINT_NULLNESS_IGNORE_DEPRECATED environment variable prevents Studio from showing IDE
514# inspection warnings for nullability issues, if the context is deprecated
515# This environment variable is consumed by InteroperabilityDetector.kt
516
517function runStudioLinux() {
518  studioPath="${studioUnzippedPath}/android-studio/bin/studio.sh"
519  updateStudioLaunchScript
520  echo "$studioPath &"
521  env LAUNCHED_VIA_STUDIOW='true' \
522    STUDIO_PROPERTIES="${studioPropertiesFile}" \
523    STUDIO_VM_OPTIONS="${scriptDir}/development/studio/studio.vmoptions" \
524    ANDROID_LINT_NULLNESS_IGNORE_DEPRECATED="true" \
525    "${studioPath}" "${projectDir}" &>/dev/null &
526}
527
528function runStudioMac() {
529  appPath="$(findStudioMacAppPath)"
530  echo "open ${appPath}"
531  env STUDIO_PROPERTIES="${studioPropertiesFile}" \
532    STUDIO_VM_OPTIONS="${scriptDir}/development/studio/studio.vmoptions" \
533    ANDROID_LINT_NULLNESS_IGNORE_DEPRECATED="true" \
534    open -a "${appPath}" "${projectDir}"
535}
536
537function runStudio() {
538  local studioPropertiesFile="${androidStudioSettingsDir}/${ideaPropRelPath}"
539  updateIdeaProperties
540  # Export an env var so the gradle script can check that Android Studio was launched using this
541  # script. Launching Android Studio and then opening the Gradle project after-the-fact IS
542  # UNSUPPORTED. Android Studio needs to be lunched using studiow so that it can validate settings
543  # and update the build environment.
544  export STUDIO_LAUNCHED_WITH_WRAPPER='true'
545  if [ "${osName}" == "mac" ]; then
546    runStudioMac
547  else
548    runStudioLinux
549  fi
550}
551
552function runCleanProjectFiles() {
553  local projects=(
554    //external/setupcompat
555    //external/setupdesign
556    //frameworks/base
557    //frameworks/libs/systemui
558    //packages/apps/Launcher3
559    //platform_testing
560    //vendor/unbundled_google/libraries
561    //vendor/unbundled_google/packages/NexusLauncher
562    //packages/apps/ManagedProvisioning
563  )
564  local gitPath
565  for gitPath in "${projects[@]}"; do
566    cleanProjectFiles "$gitPath"
567  done
568  removeDirIfExists "${gradleOutDir}/build" || :
569}
570
571function cleanProjectFiles() {
572  local projectPath="$1"
573  local gitPath="${androidBuildTop}/${gitPath:1}"
574  local cleanPreview="$(git -C "$gitPath" clean --dry-run --force -X -d .)"
575  if [[ -z "$cleanPreview" ]]; then
576    echo "$projectPath already clean. Nothing to do."
577  else
578    echo "$projectPath cleaning:"
579    echo "$cleanPreview"
580    if yesNo 'Do you want to delete these files?' 'n'; then
581      git -C "$gitPath" clean --force -X -d .
582    else
583      echo "Clean operation cancelled."
584    fi
585  fi
586}
587
588function removeDirIfExists() {
589  local dir="$1"
590  if [[ -z "${dir}" ]] ; then
591    echo 'script error: removeDirIfExists expects 1 arg'
592    exit 1
593  fi
594  if [[ -d "${dir}" ]] ; then
595    if yesNo "Remove ${dir}?" 'n'; then
596      rm -rf "${dir}"
597    fi
598    return 0
599  fi
600  return 1
601}
602
603function runUninstallAndroidStudio() {
604  if ! yesNo 'This will remove the local Android Studio installation, local SDK, and project gradle files. Proceed?'; then
605    echo "Uninstall operation cancelled."
606    return
607  fi
608  removeDirIfExists "$studioHomeDir" || echo "Android Studio installation not found."
609  removeDirIfExists "$(getLegacySdkDir)" || :
610  removeDirIfExists "$(getLegacySdkDir2)" || :
611  removeDirIfExists "${gradleOutDir}" || :
612
613  runCleanProjectFiles
614}
615
616function adbGetProp() {
617  local prop="$1"
618  echo "$(adb shell getprop $prop 2>/dev/null || true)"
619}
620
621function askToUpdateSdkUsingAdb() {
622  echo ''
623  if yesNo "Update SDK using adb?"; then
624    updateSdk="adb"
625  fi
626}
627
628function checkSdkNeedsUpdate() {
629  printInfo "Android SDK Location: $sdkDir"
630  local localSdkTime="$(getProperyValueAbs $platformDir/build.prop ro.system.build.date.utc)"
631  local localSdkFingerprint="$(getProperyValueAbs $platformDir/build.prop ro.system.build.fingerprint)"
632  local utcTimeOneWeekAgo="$(expr $(date +%s) - 604800)"
633  if [[ -z "$localSdkFingerprint" ]]; then
634    localSdkFingerprint='<Unknown>'
635  fi
636
637  local sdkGenSrc="$(getProperyValueAbs $platformDir/build.prop ro.sdk.gensrc)"
638  sdkGenSrc="${sdkGenSrc:-adb}"
639  printInfo "Android SDK Fingerprint: $localSdkFingerprint"
640  printInfo "Android SDK was last updated using \"--update-sdk ${sdkGenSrc}\""
641  if [[ -z "$localSdkTime" ]]; then
642    printWarning 'Could not determine age of Android SDK. This means your SDK is corrupt or out of date.'
643    askToUpdateSdkUsingAdb
644  elif [[ "$utcTimeOneWeekAgo" -gt "$localSdkTime" ]]; then
645    printWarning 'Android SDK is more than 7 days old.'
646    askToUpdateSdkUsingAdb
647  else
648    local serial="$(adb get-serialno 2> /dev/null)"
649    if [[ -z "$serial" ]]; then
650      local noDeviceMessage='Skipping adb build check. No devices/emulators found.'
651      if [[ "$sdkGenSrc" == 'adb' ]]; then
652        printWarning "$noDeviceMessage"
653      else
654        printInfo "$noDeviceMessage"
655      fi
656    else
657      local adbSdkBuildTime="$(adbGetProp ro.system.build.date.utc)"
658      if [ "${updateSdk}" != "true" ]; then
659        if [ $adbSdkBuildTime -gt $localSdkTime ]; then
660          printWarning "Attached device has newer SDK."
661          askToUpdateSdkUsingAdb
662        fi
663      fi
664    fi
665  fi
666}
667
668function createPackageXml() {
669  local targetFile="$1"
670  local localPackage="$2"
671  local buildToolsVersion="$3"
672  local typeDetails="$4"
673  local displayName="$5"
674
675  # Split X.Y.Z-rcN into an array of X, Y, Z, and rcN
676  IFS='.-'
677  local versionNumbers=($buildToolsVersion)
678  unset IFS
679  local majorVersionString=""
680  local minorVersionString=""
681  local microVersionString=""
682  local previewVersionString=""
683  if [[ -n "${versionNumbers[0]}" ]]; then
684    majorVersionString="<major>${versionNumbers[0]}</major>"
685  fi
686  if [[ -n "${versionNumbers[1]}" ]]; then
687    minorVersionString="<minor>${versionNumbers[1]}</minor>"
688  fi
689  if [[ -n "${versionNumbers[2]}" ]]; then
690    microVersionString="<micro>${versionNumbers[2]}</micro>"
691  fi
692  # preview version includes rc if it's set, e.g. rc1. It could also be an empty string if it's not
693  # a preview version at all
694  if [[ -n "${versionNumbers[3]}" ]]; then
695    # Remove rc from the preview version number
696    previewVersionString="<preview>${versionNumbers[3]#rc}</preview>"
697  fi
698
699  ( cat <<'EOF'
700<?xml version="1.0" encoding="UTF-8" standalone="yes"?><ns2:repository xmlns:ns2="http://schemas.android.com/repository/android/common/02" xmlns:ns3="http://schemas.android.com/repository/android/common/01" xmlns:ns4="http://schemas.android.com/repository/android/generic/01" xmlns:ns5="http://schemas.android.com/repository/android/generic/02" xmlns:ns6="http://schemas.android.com/sdk/android/repo/addon2/01" xmlns:ns7="http://schemas.android.com/sdk/android/repo/addon2/02" xmlns:ns8="http://schemas.android.com/sdk/android/repo/addon2/03" xmlns:ns9="http://schemas.android.com/sdk/android/repo/repository2/01" xmlns:ns10="http://schemas.android.com/sdk/android/repo/repository2/02" xmlns:ns11="http://schemas.android.com/sdk/android/repo/repository2/03" xmlns:ns12="http://schemas.android.com/sdk/android/repo/sys-img2/03" xmlns:ns13="http://schemas.android.com/sdk/android/repo/sys-img2/02" xmlns:ns14="http://schemas.android.com/sdk/android/repo/sys-img2/01"><license id="android-sdk-license" type="text">Terms and Conditions
701
702This is the Android Software Development Kit License Agreement
703</license>
704EOF
705) > "$targetFile"
706echo "<localPackage path=\"$localPackage\" obsolete=\"false\">
707    <type-details xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"ns11:platformDetailsType\">$typeDetails</type-details>
708    <revision>
709      $majorVersionString
710      $minorVersionString
711      $microVersionString
712      $previewVersionString
713    </revision>
714    <display-name>$displayName</display-name>
715    <uses-license ref=\"android-sdk-license\"/>
716  </localPackage>
717</ns2:repository>
718" >> $targetFile
719}
720
721function appendLineIfNotExists() {
722  grep -qxF "$1" $2 || echo "$1" >> $2
723}
724
725function setupMockSdk() {
726  rm -rf $sdkDir
727  mkdir -p $sdkDir
728
729  local toolsName
730  if [ "${osName}" == "mac" ]; then
731    toolsName="darwin"
732  else
733    toolsName="linux"
734  fi
735
736  # e.g. 33.0.1 OR 34.0.0-rc1
737  # In Gradle, there's a space before 'rc', but when packaged in Android/Sdk, there's a dash.
738  # To work with that, replace ' ' with '-' for usage in the script
739  local buildToolsVersion="$(getProperyValue ManagedProvisioningGradleProject/gradle.properties BUILD_TOOLS_VERSION | sed 's/ /-/')"
740
741  local adbRelPath="out/host/$toolsName-x86/bin/adb"
742  local compiledAdb="$androidBuildTop/$adbRelPath"
743  # platform tools
744  if [ ! -f $compiledAdb ]; then
745    # Check if adb exists in path (don't exit if `which` fails)
746    compiledAdb="$(which adb || true)"
747    if [ ! -f $compiledAdb ]; then
748      printError "Could not find adb at $adbRelPath or in the environment path"
749      echo "Did you build system image once?"
750      exit 1
751    fi
752  fi
753  printInfo "Using adb binary found at $compiledAdb"
754  mkdir $sdkDir/platform-tools
755  ln -s $compiledAdb $sdkDir/platform-tools/adb
756  createPackageXml $sdkDir/platform-tools/package.xml "platform-tools" "$buildToolsVersion" "" "Android SDK Platform-Tools"
757
758  # Setup build tools
759  mkdir -p $sdkDir/build-tools
760  cp -r $prebuiltsDir/sdk/tools/$toolsName/bin $sdkDir/build-tools/$buildToolsVersion
761  cp -r $prebuiltsDir/sdk/tools/$toolsName/lib $sdkDir/build-tools/$buildToolsVersion/lib
762  cp -r $prebuiltsDir/sdk/tools/$toolsName/lib64 $sdkDir/build-tools/$buildToolsVersion/lib64
763  ln -s $prebuiltsDir/sdk/renderscript $sdkDir/build-tools/$buildToolsVersion/renderscript
764  # All tools are now validated by studio, so we need to collect them all
765  cp -r $prebuiltsDir/sdk/tools/lib/* $sdkDir/build-tools/$buildToolsVersion/lib
766  cp $prebuiltsDir/sdk/tools/dx $sdkDir/build-tools/$buildToolsVersion
767
768  cp -r $prebuiltsDir/build-tools/$toolsName-x86/bin/* $sdkDir/build-tools/$buildToolsVersion/
769  cp -r $prebuiltsDir/build-tools/$toolsName-x86/lib64/* $sdkDir/build-tools/$buildToolsVersion/lib64/
770  createPackageXml $sdkDir/build-tools/$buildToolsVersion/package.xml "build-tools;$buildToolsVersion" "$buildToolsVersion" "" "Android SDK Build-Tools $buildToolsVersion"
771
772  # Setup platforms
773  mkdir -p $platformDir
774  createPackageXml $platformDir/package.xml "platforms;$compileSdkVersion" "1" "<api-level>$sdkVersionInt</api-level><codename>$compileSdkPreview</codename><layoutlib api=\"15\" />" "Android SDK Platform $upperCaseBranchName for SysUiStudio (tree=$androidBuildTop, branch=$branchName, treeId=$androidTreeUniqueId)"
775
776  prebuiltAndroidJar=$prebuiltsDir/sdk/current/system/android.jar
777
778  # Setup test and optional packages
779  mkdir $platformDir/optional
780  cp -r $prebuiltsDir/sdk/current/test/*.*.jar $platformDir/optional/
781  list=
782  for f in $platformDir/optional/*.jar; do
783    filename=$(basename -- "$f")
784    libname="${filename%.*}"
785
786    [ ! -z "$list" ] && list="$list,"
787    br=$'\n'
788    list="$list$br{ \"name\": \"$libname\", \"jar\": \"$filename\", \"manifest\": false }"
789  done
790  echo "[$list]" > $platformDir/optional/optional.json
791}
792
793function createPlatformSdk() {
794  assertHasAdbRoot
795  echo "Setting up SDK from scratch"
796  setupMockSdk
797
798  echo "Updating private apis sdk"
799  local android_jar_outfile="$platformDir/android.jar"
800  local framework_aidl="$platformDir/framework.aidl"
801  rm -rf $android_jar_outfile
802
803  local buildPropFile="$platformDir/build.prop"
804  local buildDate=
805  local buildFingerprint=
806
807  if [[ "$updateSdk" == 'adb' ]]; then
808    local tempFiles="$(mktemp -d)"
809
810    buildDate="$(adbGetProp ro.system.build.date.utc)"
811    buildFingerprint="$(adbGetProp ro.system.build.fingerprint)"
812
813    printInfo "Pulling SDK artifacts from device $buildFingerprint using adb"
814    adb pull /system/framework/framework-res.apk $tempFiles/
815    adb pull /system/framework/framework.jar $tempFiles/
816    adb pull /system/framework/framework-location.jar $tempFiles/
817
818    local dexList="--dex $tempFiles/framework.jar"
819    dexList="$dexList --dex $tempFiles/framework-location.jar"
820    local apexJars=($(adb shell ls /apex/*/javalib/*.jar))
821    for f in "${apexJars[@]}"
822    do
823      local fileBasename="$(basename $f)"
824      if [[ ! $f = *@* ]] && [[ "$fileBasename" != service-* ]]; then
825        local target="$tempFiles/$(basename $f)"
826        adb pull $f $target
827        dexList="$dexList --dex $target"
828      fi
829    done
830
831    cp $prebuiltsDir/sdk/current/public/framework.aidl $framework_aidl
832
833    java -jar $scriptDir/StubGenerator/StubGenerator.jar -o $android_jar_outfile \
834        $dexList \
835        --zip $tempFiles/framework-res.apk \
836        --zip $prebuiltAndroidJar \
837        --aidl $framework_aidl
838
839    echo "Removing temp files"
840    rm -rf $tempFiles
841
842    cp $prebuiltsDir/sdk/current/module-lib/core-for-system-modules.jar "$platformDir"
843  elif [[ "$updateSdk" == 'soong' ]]; then
844    printInfo "Building SDK artifacts using soong"
845
846    # TODO(b/251871740): Replace these steps with the output of a soong target
847    #
848    # ---- begin ----
849    #
850    local frameworks_deps=(
851      out/soong/.intermediates/build/soong/java/core-libraries/legacy.core.platform.api.stubs/android_common/turbine-combined/legacy.core.platform.api.stubs.jar
852      out/soong/.intermediates/libcore/core-lambda-stubs-for-system-modules/android_common/turbine-combined/core-lambda-stubs-for-system-modules.jar
853      out/soong/.intermediates/libcore/core-generated-annotation-stubs/android_common/turbine-combined/core-generated-annotation-stubs.jar
854      out/soong/.intermediates/frameworks/base/framework/android_common/turbine-combined/framework.jar
855      out/soong/.intermediates/frameworks/base/ext/android_common/turbine-combined/ext.jar
856      out/soong/.intermediates/frameworks/base/core/res/framework-res/android_common/package-res.apk
857      out/soong/.intermediates/tools/metalava/private-stub-annotations-jar/android_common/turbine-combined/private-stub-annotations-jar.jar
858    )
859
860    cd $androidBuildTop
861
862    . ./build/envsetup.sh
863    if [[ -n "$TARGET_PRODUCT" && -n "$TARGET_BUILD_VARIANT" ]]; then
864      if [[ -n "$TARGET_RELEASE" ]]; then
865        lunch "$TARGET_PRODUCT-$TARGET_RELEASE-$TARGET_BUILD_VARIANT"
866      else
867        lunch "$TARGET_PRODUCT-$TARGET_BUILD_VARIANT"
868      fi
869    else
870      lunch aosp_arm64-trunk_staging-userdebug
871    fi
872
873    m merge_zips sdkparcelables ${frameworks_deps[@]}
874
875    echo "Updating private apis sdk"
876    merge_zips --ignore-duplicates $android_jar_outfile ${frameworks_deps[@]}
877
878    sdkparcelables "$android_jar_outfile" "$framework_aidl"
879    # TODO(b/259594098): sdkparcelables output aidl file doesn't include all the interfaces we need
880    appendLineIfNotExists "interface android.app.IApplicationThread;" "$framework_aidl"
881    appendLineIfNotExists "interface android.view.IRecentsAnimationController;" "$framework_aidl"
882    appendLineIfNotExists "interface android.view.IRecentsAnimationRunner;" "$framework_aidl"
883    appendLineIfNotExists "interface android.view.IRemoteAnimationRunner;" "$framework_aidl"
884    appendLineIfNotExists "interface android.window.IOnBackInvokedCallback;" "$framework_aidl"
885
886    cp $prebuiltsDir/sdk/current/module-lib/core-for-system-modules.jar "$platformDir"
887    #
888    # ---- end ----
889    #
890    buildDate="$(date +%s)"
891    buildFingerprint="$(cat $ANDROID_PRODUCT_OUT/build_fingerprint.txt)"
892
893    printInfo "If you'd like to use this SDK on your laptop too, run ./studiow --update-sdk copy $(hostname):$platformDir"
894  elif [[ "${updateSdk}" == 'copy' ]]; then
895    scp "${copySdkSourceDir}"/{framework.aidl,android.jar,build.prop,core-for-system-modules.jar} "$platformDir/."
896    # Copy the build date and fingerprint from build.prop, then delete the file
897    buildDate="$(getProperyValueAbs $buildPropFile ro.system.build.date.utc)"
898    buildFingerprint="$(getProperyValueAbs $buildPropFile ro.system.build.fingerprint)"
899    rm "$buildPropFile"
900  else
901    printError "Internal error. Unknown SDK update source: $updateSdk"
902    exit 1
903  fi
904
905  {
906    echo "ro.system.build.version.sdk=$sdkVersionInt"
907    echo "ro.build.version.codename=$compileSdkPreview"
908    echo "ro.system.build.date.utc=$buildDate"
909    echo "ro.system.build.fingerprint=$buildFingerprint"
910    echo "ro.sdk.gensrc=$updateSdk"
911  } > "$buildPropFile"
912
913  cp "${scriptDir}/development/sdk/sdk.properties" "${platformDir}"
914
915  ( cat <<EOF
916Pkg.Desc=Android SDK Platform $upperCaseBranchName
917Pkg.UserSrc=false
918Platform.Version=$compileSdkPreview
919Platform.CodeName=$compileSdkPreview
920Pkg.Revision=1
921AndroidVersion.ApiLevel=$sdkVersionInt
922AndroidVersion.CodeName=$compileSdkPreview
923AndroidVersion.ExtensionLevel=5
924AndroidVersion.IsBaseSdk=true
925Layoutlib.Api=15
926Layoutlib.Revision=1
927Platform.MinToolsRev=22
928EOF
929) > "${platformDir}/source.properties"
930
931
932  echo "Generating sdk data"
933  # Sympolic link does not work here with android
934  mkdir -p $platformDir/data
935  cp -r $androidBuildTop/frameworks/base/core/res/res $platformDir/data/
936  mv $platformDir/data/res/values/public-final.xml $platformDir/data/res/values/public.xml
937
938  echo "Removing build cache"
939  rm -rf "${scriptDir}/ManagedProvisioningGradleProject/.build-cache"
940
941  echo "Accepting license"
942  cp -r ${prebuiltsDir}/cmdline-tools $sdkDir/
943  yes | $sdkDir/cmdline-tools/tools/bin/sdkmanager --licenses >/dev/null
944  rm -rf $sdkDir/cmdline-tools
945
946  echo "Linking sources"
947  ln -s $androidBuildTop/frameworks/base/core/java $platformDir/sources
948
949  echo "Done"
950}
951
952function copyFileIfAbsent() {
953  SRC=$1
954  DEST=$2
955  if [ ! -f $DEST ]; then
956    mkdir -p $(dirname $DEST)
957    cp $SRC $DEST
958  fi
959}
960
961# On mac, a newer JDK must be installed manually.
962function checkJdkVersion() {
963  if [ "${osName}" == "mac" ]; then
964    local javaHome
965    javaHome="$(/usr/libexec/java_home -v 17)"
966    if [[ "$?" -eq 0 && -n "$javaHome" ]]; then
967      return 0
968    else
969      printError "Compatible JDK not found. studiow requires JDK 17 to run Android's command-line tools for generating the SDK. Install the JDK using 'mule install jdk19' and re-run this script."
970      exit
971    fi
972  fi
973}
974
975function updateLocalGradleProperties() {
976  export ANDROID_HOME="${sdkDir}"
977  ( cat <<EOF
978sdk.dir=${sdkDir}
979EOF
980) > "$projectDir/local.properties"
981  ( cat <<EOF
982compile.sdk.preview=${compileSdkPreview}
983compile.sdk.version=${compileSdkVersion}
984EOF
985) > "$projectDir/studiow.properties"
986}
987
988function updateStudioConfig() {
989  local legacy_config_dir="${studioHomeDir}/config"
990  local config_dir="${androidStudioSettingsDir}/config"
991  if [[ "${usingDefaultSettingsDir}" == 'true' && -d "$legacy_config_dir" ]]; then
992    local message=(
993      '\n-------------------------------------------------------------------------------------\n\n'
994      "${YELLOW}WARNING:${NC} You have config files stored in ~/.AndroidStudioSystemUI/config "
995      'which will NOT be used by this instance of sysui-studio. '
996      'To support multi-branch development, '
997      "the config directory has moved to \$TOP${config_dir#"$androidBuildTop"}"
998      '\n\n'
999      'Each tree now has its own config directory. This means you may need to change your '
1000      'settings for Android Studio multiple times (per branch) if, for example, you want to '
1001      'change the default keymap.'
1002      '\n\n'
1003      'Your existing configs have NOT been automatically moved or deleted for the following '
1004      'reasons:'
1005      '\n\n'
1006      '  1) You may want to inspect your old settings\n'
1007      '  2) The old config may still be used by branches on revisions predate this change.'
1008      '\n\n'
1009      'For more info, see http://b/249826650'
1010      '\n\n'
1011      'To make this warning go away, delete ~/.AndroidStudioSystemUI/config'
1012      '\n\n'
1013      'To launch Android Studio with the old config, run the following:'
1014      '\n'
1015      '\n    studiow --settings-dir ~/.AndroidStudioSystemUI'
1016      '\n'
1017      '\nYou can then export your old settings using File > Manage IDE Settings > Export Settings'
1018      '\n'
1019      '\n-------------------------------------------------------------------------------------\n\n'
1020
1021    )
1022    local fmt_cmd=(fmt -w 100 --split-only)
1023    if [ "$osName" == "mac" ]; then
1024      fmt_cmd=(fmt -w 100)
1025    fi
1026    printf "$(printf %s "${message[@]}")" | ${fmt_cmd[@]}
1027  fi
1028
1029  mkdir -p "${config_dir}"
1030  # Disable update checks
1031  copyFileIfAbsent $scriptDir/development/studio/updates.xml "${androidStudioSettingsDir}/updates.xml"
1032  # Disable instant run
1033  copyFileIfAbsent $scriptDir/development/studio/instant-run.xml $config_dir/options/instant-run.xml
1034  # Copy android code style
1035  copyFileIfAbsent $scriptDir/development/studio/AndroidStyle.xml $config_dir/codestyles/AndroidStyle.xml
1036  copyFileIfAbsent $scriptDir/development/studio/code.style.schemes.xml $config_dir/options/code.style.schemes.xml
1037  # Disable notification to update gradle
1038  copyFileIfAbsent $scriptDir/development/studio/notifications.xml $config_dir/options/notifications.xml
1039
1040  # Disable dialog that asks to trust the project
1041  local trustedPathsFile="$config_dir/options/trusted-paths.xml"
1042  if [ ! -f $trustedPathsFile ]; then
1043    sed "s|__PROJECT_DIR_PLACEHOLDER__|$projectDir|" $scriptDir/development/studio/trusted-paths.xml > $trustedPathsFile
1044  fi
1045
1046  # Disable dialog that asks whether to use Studio SDK or Project SDK
1047  local jdkPathsFile="$config_dir/options/jdk.table.xml"
1048  if [ ! -f $jdkPathsFile ]; then
1049    sed "s|__SDK_DIR_PLACEHOLDER__|$sdkDir|g;s|__SDK_VERSION_NAME_PLACEHOLDER__|$compileSdkVersion|g;" $scriptDir/development/studio/jdk.table.xml > $jdkPathsFile
1050  fi
1051}
1052
1053function main() {
1054  parseOptions "$@"
1055
1056  if [ "${uninstallAndroidStudio}" == "true" ]; then
1057    runUninstallAndroidStudio
1058    exit
1059  fi
1060
1061  if [ "${cleanProjectFiles}" == "true" ]; then
1062    runCleanProjectFiles
1063    exit
1064  fi
1065
1066  assertHasAdbRoot
1067  setupBuildSrcSymlinks
1068
1069  if [ "${downloadStudioZip}" == "true" ]; then
1070    fetchStudioUrl
1071
1072    studioDestName="$(basename ${studioUrl})"
1073    studioZipPath="${studioSetupDir}/${studioDestName}"
1074    studioUnzippedPath="$(echo ${studioZipPath} | sed 's/\.zip$//' | sed 's/\.tar\.gz$//')"
1075  fi
1076
1077  # Checks if a platform SDK already exists. If it doesn't, automatically adjust the flags so that
1078  # an SDK is generated by pulling files off the device.
1079  if [[ -z "$updateSdk" ]]; then
1080    if [[ ! -f $platformDir/android.jar ]]; then
1081      printInfo "Android SDK not found. Automatically appending '--update-sdk adb' to the argument list."
1082      updateSdk='adb'
1083    else
1084      checkSdkNeedsUpdate
1085    fi
1086  fi
1087
1088  checkJdkVersion
1089
1090  if [ -n "${updateSdk}" ]; then
1091    createPlatformSdk
1092    updateLocalGradleProperties
1093  fi
1094
1095  if [ "${downloadStudioZip}" == "true" ]; then
1096    updateStudio
1097    createDesktopEntry
1098  fi
1099  if [ "${runStudio}" == "true" ]; then
1100    checkLicenseAgreement
1101    updateLocalGradleProperties
1102    updateStudioConfig
1103    runStudio
1104  fi
1105}
1106
1107function createDesktopEntry() {
1108  studiow_path="${scriptDir}/studiow"
1109  studiow_icon="${studioUnzippedPath}/android-studio/bin/studio.png"
1110  targetDir="${HOME}/.local/share/applications"
1111  mkdir -p "$targetDir"
1112  cat "${scriptDir}/MPStudio.desktop" | \
1113    sed "s|%STUDIOW_PATH%|${studiow_path}|g" | \
1114    sed "s|%ANDROID_TOP%|${androidBuildTop}|g" | \
1115    sed "s|%STUDIOW_ICON%|${studiow_icon}|g" > "${targetDir}/MPStudio.desktop"
1116}
1117
1118main "$@"
1119