๐ค Android
AAPT and GZ files¶
AAPT will silently decompress any .gz
file from the assets
directory.
If you store a (compressed) /assets/data.json.gz
file, AAPT will only package a (decompressed) /assets/data.json
file in the resulting apk and aab.
A workaround is to move the file in Java's resources folder /resources/assets/data.json.gz
.
AAPT ignore assets from third party libraries¶
android {
androidResources {
ignoreAssetsPatterns += "SomeFont.ttf"
}
}
ADB with fzf¶
adb devices -l 2> /dev/null `: # list devices and ignore daemon messages` |
tail -n +2 `: # ignore first line` |
head -n -1 `: # ignore last line` |
cut -d " " -f1 `: # extract device id` |
fzf --header '๐ฑ Select one or more devices' --border --reverse --multi \
--preview-window="right:66%" \
--preview="adb -s {} shell getprop | grep -E 'ro.build.(description|fingerprint|version.(release|sdk))|ro.product.(cpu.abi|device|locale|manufacturer|model|name)'" |
xargs --no-run-if-empty --verbose -I % adb -s % `: # run your command of choice`
adb devices -l 2> /dev/null `: # list devices and ignore daemon messages` |
tail -n +2 `: # ignore first line` |
ghead -n -1 `: # ignore last line` |
cut -d " " -f1 `: # extract device id` |
fzf --header '๐ฑ Select one or more devices' --border --reverse --multi \
--preview-window="right:66%" \
--preview="adb -s {} shell getprop | grep -E 'ro.build.(description|fingerprint|version.(release|sdk))|ro.product.(cpu.abi|device|locale|manufacturer|model|name)'" |
xargs -t -I % adb -s % `: # run your command of choice`
> <search> โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
3/3 โ [ro.build.description]: [sdk_gphone64_x86_64-user 14 U โ
Select one or more devices โ [ro.build.fingerprint]: [google/sdk_gphone64_x86_64/em โ
18261FDEE000YJ โ [ro.build.version.release]: [14] โ
> emulator-5554 โ [ro.build.version.release_or_codename]: [14] โ
emulator-5556 โ [ro.build.version.release_or_preview_display]: [14] โ
โ [ro.build.version.sdk]: [34] โ
โ [ro.product.cpu.abi]: [x86_64] โ
โ [ro.product.cpu.abilist]: [x86_64,arm64-v8a] โ
โ [ro.product.cpu.abilist32]: [] โ
โ [ro.product.cpu.abilist64]: [x86_64,arm64-v8a] โ
โ [ro.product.device]: [emu64xa] โ
โ [ro.product.locale]: [en-US] โ
โ [ro.product.manufacturer]: [Google] โ
โ [ro.product.model]: [sdk_gphone64_x86_64] โ
โ [ro.product.name]: [sdk_gphone64_x86_64] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
This can also be used as an alias, for example adbz
function adbz() {
adb devices -l 2> /dev/null `: # list devices and ignore daemon messages` |
tail -n +2 `: # ignore first line` |
head -n -1 `: # ignore last line` |
cut -d " " -f1 `: # extract device id` |
fzf --header '๐ฑ Select one or more devices' --border --reverse --multi \
--preview-window="right:66%" \
--preview="adb -s {} shell getprop | grep -E 'ro.build.(description|fingerprint|version.(release|sdk))|ro.product.(cpu.abi|device|locale|manufacturer|model|name)'" |
xargs --no-run-if-empty --verbose -I % adb -s % "$@" `: # run the command with provided arguments`
}
function adbz() {
adb devices -l 2> /dev/null `: # list devices and ignore daemon messages` |
tail -n +2 `: # ignore first line` |
ghead -n -1 `: # ignore last line` |
cut -d " " -f1 `: # extract device id` |
fzf --header '๐ฑ Select one or more devices' --border --reverse --multi \
--preview-window="right:66%" \
--preview="adb -s {} shell getprop | grep -E 'ro.build.(description|fingerprint|version.(release|sdk))|ro.product.(cpu.abi|device|locale|manufacturer|model|name)'" |
xargs -t -I % adb -s % adb -s % "$@" `: # run your command of choice`
}
Then simply execute your regular adb
command with adbz install app.apk
APK MIME type¶
application/vnd.android.package-archive
App Links¶
Quote
To test an existing statement file, you can use the official Statement List Generator and Tester tool.
During app install/update, an Android service will verify if the App Links configuration complies with the server side assetlinks.json
file.
The results will be sent to logcat, with these tags: IntentFilterIntentSvc
and SingleHostAsyncVerifier
.
The following command will filter the appropriate logcat messages:
adb logcat -s "IntentFilterIntentSvc:*" "SingleHostAsyncVerifier:*" "AppLinksAsyncVerifierV2:*" "AppLinksHostsVerifierV2:*"
Logcat
Success
AppLinksAsyncVerifierV2 I Verification result: checking for a statement with source https://smarquis.fr, relation delegate_permission/common.handle_all_urls, and target fr.smarquis.applinks --> true. [CONTEXT service_id=244 ]
AppLinksHostsVerifierV2 I Verification fr.smarquis.applinks complete. Successful hosts: smarquis.fr. Failed hosts: . Error hosts: . [CONTEXT service_id=244 ]
Failure
AppLinksAsyncVerifierV2 I Verification result: checking for a statement with source https://smarquis.fr, relation delegate_permission/common.handle_all_urls, and target fr.smarquis.applinks --> false. [CONTEXT service_id=244 ]
AppLinksHostsVerifierV2 I Verification fr.smarquis.applinks complete. Successful hosts: . Failed hosts: smarquis.fr. Error hosts: . [CONTEXT service_id=244 ]
Success
I/IntentFilterIntentSvc: Verifying IntentFilter. verificationId:0 scheme:"https" hosts:"smarquis.fr" package:"fr.smarquis.applinks".
I/SingleHostAsyncVerifier: Verification result: checking for a statement with source a <
a: "https://smarquis.fr"
>
, relation delegate_permission/common.handle_all_urls, and target b <
a: "fr.smarquis.applinks"
b <
a: "D2:18:2B:0E:34:38:3B:FD:A7:80:AC:21:88:F1:F7:1F:13:33:AD:CB:E3:94:2A:75:96:FB:A1:7A:0B:6B:CE:68"
>
>
--> true.
I/IntentFilterIntentSvc: Verification 0 complete. Success:true. Failed hosts:.
Failure
I/IntentFilterIntentSvc: Verifying IntentFilter. verificationId:1 scheme:"https" hosts:"smarquis.fr" package:"fr.smarquis.applinks".
I/SingleHostAsyncVerifier: Verification result: checking for a statement with source a <
a: "https://smarquis.fr"
>
, relation delegate_permission/common.handle_all_urls, and target b <
a: "fr.smarquis.applinks"
b <
a: "D2:18:2B:0E:34:38:3B:FD:A7:80:AC:21:88:F1:F7:1F:13:33:AD:CB:E3:94:2A:75:96:FB:A1:7A:0B:6B:CE:68"
>
>
--> false.
I/IntentFilterIntentSvc: Verification 1 complete. Success:false. Failed hosts:smarquis.fr.
ADB also offers some commands to troubleshoot App Links:
- Reset the state of App Links
adb shell pm set-app-links --package <package.name> 0 all
- Restart the verification process
adb shell pm verify-app-links --re-verify <package.name>
- Dump the current status
adb shell pm get-app-links <package.name>
AutoCleanedValue¶
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle.State.INITIALIZED
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class AutoCleanedValue<T : Any>(
fragment: Fragment,
private val initializer: (() -> T)?
) : ReadWriteProperty<Fragment, T> {
private var _value: T? = null
init {
fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
val viewLifecycleOwnerObserver = Observer<LifecycleOwner> {
it?.lifecycle?.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
_value = null
}
})
}
override fun onCreate(owner: LifecycleOwner) =
fragment.viewLifecycleOwnerLiveData.observeForever(viewLifecycleOwnerObserver)
override fun onDestroy(owner: LifecycleOwner) =
fragment.viewLifecycleOwnerLiveData.removeObserver(viewLifecycleOwnerObserver)
})
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T = _value ?: run {
if (!thisRef.viewLifecycleOwner.lifecycle.currentState.isAtLeast(INITIALIZED)) throw IllegalStateException("Fragment might have been destroyed or is not initialized yet!")
if (initializer == null) throw IllegalStateException("No default initializer provided!")
initializer.invoke().also { _value = it }
}
override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
_value = value
}
}
fun <T : Any> Fragment.autoCleaned(
initializer: (() -> T)? = null
): AutoCleanedValue<T> = AutoCleanedValue(this, initializer)
class MyFragment : Fragment() {
private val adapter: MyAdapter by autoCleaned(::MyAdapter)
private var binding: MyBinding by autoCleaned()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = MyBinding.inflate(inflater, container, false).also { binding = it }.root
}
BottomSheetBehavior extensions¶
fun <V : View> V.behavior(): BottomSheetBehavior<V> = BottomSheetBehavior.from(this)
fun BottomSheetBehavior<*>.isExpanded(): Boolean = state == STATE_EXPANDED
fun BottomSheetBehavior<*>.isHalfExpanded(): Boolean = state == STATE_HALF_EXPANDED
fun BottomSheetBehavior<*>.isCollapsed(): Boolean = state == STATE_COLLAPSED
fun BottomSheetBehavior<*>.isHidden(): Boolean = state == STATE_HIDDEN
fun BottomSheetBehavior<*>.isDragging(): Boolean = state == STATE_DRAGGING
fun BottomSheetBehavior<*>.isSettling(): Boolean = state == STATE_SETTLING
fun BottomSheetBehavior<*>.collapse() { state = STATE_COLLAPSED }
fun BottomSheetBehavior<*>.expand() { state = STATE_EXPANDED }
fun BottomSheetBehavior<*>.halfExpand() { state = STATE_HALF_EXPANDED }
fun BottomSheetBehavior<*>.hide() { state = STATE_HIDDEN }
fun BottomSheetBehavior<*>.toggle() { if (isExpanded()) collapse() else expand() }
/**
* @param onBottomSheetStateChanged [BottomSheetCallback.onStateChanged]
*/
fun BottomSheetBehavior<*>.addBottomSheetStateChangedCallback(onBottomSheetStateChanged: (bottomSheet: View, newState: Int) -> Unit): BottomSheetCallback = object : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, @BottomSheetBehavior.State newState: Int) = onBottomSheetStateChanged(bottomSheet, newState)
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
}.also(this::addBottomSheetCallback)
/**
* @param onBottomSheetSlide [BottomSheetCallback.onSlide]
*/
fun BottomSheetBehavior<*>.addBottomSheetSlideCallback(onBottomSheetSlide: (bottomSheet: View, slideOffset: Float) -> Unit): BottomSheetCallback = object : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, @BottomSheetBehavior.State newState: Int) = Unit
override fun onSlide(bottomSheet: View, slideOffset: Float) = onBottomSheetSlide(bottomSheet, slideOffset)
}.also(this::addBottomSheetCallback)
ConcatAdapter find global position¶
/**
* @return the (global) position for the (local) [position] of the [adapter], or [NO_POSITION] if the [adapter] is not part of [this].
*/
fun ConcatAdapter.findPositionOf(
position: Int,
adapter: Adapter<*>,
): Int {
var offset = 0
adapters.forEach {
if (it == adapter) return offset + position
offset += it.itemCount
}
return NO_POSITION
}
Debuggable APK¶
aapt dump badging <path-to-apk> | grep -c application-debuggable
Deeplinks¶
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "https://example.com"
Or shorter:
adb shell "am start 'https://example.com'"
Note: Nested double/simple quotes might be necessary if the url contains special characters.
Disable Samsung VR¶
adb shell pm hide com.samsung.android.hmt.vrsvc
Disable unnecessary instrumented tests¶
/**
* Disable unnecessary Android instrumented tests for the [project] if there is no `androidTest` folder.
* Otherwise, these projects would be compiled, packaged, installed and ran only to end-up with the following message:
*
* > Starting 0 tests on AVD
*
* Note: this could be improved by checking other potential sourceSets based on buildTypes and flavors.
*/
internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests(
project: Project,
) = beforeVariants {
val base = "${project.projectDir}/src/androidTest"
val buildTyped = base + it.buildType?.capitalized()
val flavored = base + it.flavorName?.capitalized()
val variant = flavored + it.buildType?.capitalized()
val dirs = sequenceOf(base, buildTyped, flavored, variant).map(project::file)
it.enableAndroidTest = it.enableAndroidTest && dirs.any(File::exists)
}
Download APK files¶
adb shell pm list packages 2> /dev/null `: # List packages` |
cut -d ':' -f2 `: # Extract package name` |
fzf --header '๐ฑ Select one or more packages to download' --border --reverse --multi |
xargs --no-run-if-empty -I % sh -c '
mkdir -p "%";
for path in $(adb shell pm path "%" | sed "s/package://g")
do
package=$(basename $path);
adb pull "$path" "%/$package";
done'
Edit SharePreferences¶
~ adb shell am force-stop <packageName>
~ adb shell run-as <packageName>
$ ls shared_prefs
$ cat shared_prefs/<fileName>.xml
$ echo '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<map>
<!-- Prefs here -->
</map>
' > shared_prefs/<fileName>.xml
or
cat > shared_prefs/<fileName>.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<!-- Prefs here -->
</map>
Ctrl+C
Ellipsized TextView¶
fun TextView.isEllipsized(): Boolean = layout?.let { !TextUtils.equals(text, it.text) } ?: false
Environment variables¶
export ANDROID_HOME="/Users/$USER/Library/Android/sdk"
export PATH="$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools"
export ANDROID_HOME=$HOME/android
export ANDROID_SDK_ROOT=${ANDROID_HOME}
export PATH=${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:${PATH}
Espresso AppNotIdleException¶
On API <= 21, indeterminate ProgressBar
s would animate forever and trigger the following exception when running Espresso tests:
android.support.test.espresso.AppNotIdleException: Looped for [...] iterations over 60 SECONDS. The following Idle Conditions failed .
A simple solution is to disable such ProgressBar
s by replacing their indeterminateDrawable
with a static (non-animated) Drawable
:
fun Activity.disableProgressBarAnimations() = window.decorView.rootView.disableProgressBarAnimations()
fun Fragment.disableProgressBarAnimations() = requireView().disableProgressBarAnimations()
fun View.disableProgressBarAnimations(
replacement: Drawable = ColorDrawable(Color.BLUE),
) = recursiveChildren().filterIsInstance<ProgressBar>().forEach {
it.indeterminateDrawable = replacement
}
This can also happen for AnimatedVectorDrawable
s.
Espresso extensions¶
/**
* Simplify [View] assertions by executing the [matches] predicate on the reified instance of [T].
*
* ```kotlin
* onView(withId(id)).check(matches(
* that<TextView>("has non-blank text") {
* !it.text.isNullOrBlank()
* }
* ))
* ```
*/
inline fun <reified T : View> that(
description: String? = null,
noinline matches: (T) -> Boolean,
) = object : BoundedMatcher<View, T>(T::class.java) {
override fun matchesSafely(view: T) = matches(view)
override fun describeTo(desc: Description) {
desc.appendText(description ?: matches.toString())
}
}
/**
* Simplify [ViewAction] actions by executing the [perform] block on the reified instance of [T].
*
* ```kotlin
* onView(withId(id)).perform(
* action<ImageView>("reset image drawable") {
* it.setImageDrawable(null)
* }
* )
* ```
*/
inline fun <reified T : View> action(
description: String? = null,
noinline perform: (T) -> Unit,
) = object : ViewAction {
override fun getDescription() = description ?: perform.toString()
override fun getConstraints() = isAssignableFrom(T::class.java)
override fun perform(uiController: UiController?, view: View) = perform(view as T)
}
Exclude Proguard (R8) rules from dependency¶
android {
buildTypes {
release {
optimization {
keepRules {
ignoreFrom(
"groupId:artifactId:version",
"groupId:artifactId",
)
}
}
}
}
}
Firebase Analytics debug¶
๐ Enabling debug mode forces the app to send events immediately instead of waiting for batches.
# Enable
adb shell setprop debug.firebase.analytics.app my.package.name
# Disable
adb shell setprop debug.firebase.analytics.app .none.
๐ To enable Firebase logs in logcat:
adb shell setprop log.tag.FA VERBOSE
adb shell setprop log.tag.FA-SVC VERBOSE
adb logcat -v time -s FA FA-SVC
Forcing Lint version¶
This allows to run a specific version of Lint without changing the Android Gradle plugin (AGP).
# lint = agp + 23.0.0
# https://googlesamples.github.io/android-custom-lint-rules/api-guide.html#example:samplelintcheckgithubproject/lintversion?
android.experimental.lint.version = 8.2.0-alpha07
Google Maps Outlined Marker Label¶
๐น | ๐ผ๏ธ |
---|---|
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Paint.Cap
import android.graphics.Paint.Join
import android.graphics.Paint.Style
import android.graphics.Typeface
import android.text.Layout.Alignment.ALIGN_CENTER
import android.text.StaticLayout
import android.text.TextPaint
import android.text.style.TextAppearanceSpan
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.Px
import androidx.annotation.StyleRes
import androidx.core.graphics.withTranslation
import com.google.android.gms.maps.model.Marker
/**
* Simulates Google Maps outlined labels on [Marker]s.
* The current solution uses a [StaticLayout] and draws it twice for each [onDraw] call, modifying its [TextPaint] [TextPaint.setColor] and [TextPaint.setStyle].
* </br>
* Alternate solutions are:
* - Single [android.widget.TextView] drawing twice on each [onDraw] call (must prevent infinite loop when modifying text color)
* - Single [android.widget.TextView] with [android.widget.TextView.setShadowLayer] and draw multiple times to get a more opaque shadow:
* <pre>override fun onDraw(canvas: Canvas?) = repeat(x) { super.onDraw(canvas) }</pre>
* - Custom Span using [android.text.style.MetricAffectingSpan]
* - Two stacked [android.widget.TextView]s, one with {@code paint.style = STROKE} the other with {@code paint.style = FILL}
*/
class OutlinedMarkerLabel constructor(
context: Context,
@ColorInt val textColor: Int,
@ColorInt val strokeColor: Int,
@Px val strokeWidth: Float,
@Px val maxWidth: Int = Int.MAX_VALUE,
@StyleRes val textAppearance: Int,
) : View(context) {
private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
val span = TextAppearanceSpan(context, textAppearance)
typeface = Typeface.create(span.family, span.textStyle)
textSize = span.textSize.toFloat()
strokeWidth = this@OutlinedMarkerLabel.strokeWidth
strokeCap = Cap.ROUND
strokeJoin = Join.ROUND
}
private var staticLayout: StaticLayout? = null
private fun newStaticLayout(text: String): StaticLayout? {
if (text.isBlank()) return null
// Here we could improve the requiredWidth computation a bit by going through all lines and clamp to the largest
val width = minOf(textPaint.measureText(text).toInt(), maxWidth)
return StaticLayout.Builder.obtain(text, 0, text.length, textPaint, width)
.setIncludePad(false)
.setAlignment(ALIGN_CENTER)
.build()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val layout = staticLayout ?: return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val offset = layout.paint.strokeWidth.toInt()
setMeasuredDimension(layout.width + offset, layout.height + offset)
}
override fun onDraw(canvas: Canvas?) {
with(staticLayout ?: return) {
with(paint) {
canvas?.withTranslation(strokeWidth / 2F, strokeWidth / 2F) {
color = strokeColor
style = Style.FILL_AND_STROKE
draw(canvas)
color = textColor
style = Style.FILL
draw(canvas)
}
}
}
}
fun bind(marker: Marker) {
staticLayout = newStaticLayout(marker.title.orEmpty())
requestLayout()
}
}
Gradle Managed Virtual Devices¶
testOptions {
animationsDisabled = true
managedDevices {
devices {
create<ManagedVirtualDevice>("pixel2") {
device = "Pixel 2"
apiLevel = 30
require64Bit = true
systemImageSource = "google" /* "google-atd", "aosp", or "aosp-atd" */
}
}
}
groups {
create("pixels") {
targetDevices += devices.getByName("pixel2")
}
}
}
# Run tests on a single device
gradlew pixel2DebugAndroidTest
# or on a group of devices
gradlew pixelsGroupDebugAndroidTest
Hilt navigation args¶
@Module
@InstallIn(ViewModelComponent::class)
object UserScreenNavigationArgsModule {
@Provides
@ViewModelScoped
fun providesUserInfo(ssh: SavedStateHandle) = UserInfo(id = ssh["userId"])
}
@HiltViewModel
class UserViewModel(
private val userInfo: UserInfo
) : ViewModel()
Ignore generated baseline profile¶
This generated file is generally very large (1 to 10 MiB), wastes precious indexing time and fills up search results.
plugins {
id("idea")
}
idea {
module {
excludeDirs = excludeDirs + file("src/main/baseline-prof.txt")
}
}
In-memory DataStore¶
class InMemoryDataStore<T>(initialValue: T) : DataStore<T> {
override val data: MutableStateFlow<T> = MutableStateFlow(initialValue)
override suspend fun updateData(transform: suspend (it: T) -> T) = data.updateAndGet { transform(it) }
}
Kill rogue daemons¶
jps `: # list VMs` |
grep -E -i "gradle|kotlin" `: # Kotlin & Gradle daemons` |
cut -d " " -f1 `: # extract pid` |
xargs -t -n1 kill -9
Kotlin Coroutines debug probes¶
Quote
The kotlinx-coroutines-core
artifact contains a resource file that is not required for the coroutines to operate normally and is only used by the debugger.
android {
packagingOptions {
resources.excludes += "DebugProbesKt.bin"
}
}
Lifecycle CheatSheets¶
๐ JoseAlcerreca/android-lifecycles
Lint easter egg¶
public class Hidden {
// This looks like a comment, but it's actually code that will be executed when the class is loaded!
/* \u002a\u002f static { System.out.println("Oh no!"); } \u002f\u002a */
}
Fortunately, Kotlin does not seem to be affected by this
class Hidden {
// This is correctly parsed as a comment
/* \u002A\u002F companion object { init { println("Oh no!") } } \u002F\u002A */
}
Lint Issue Index¶
๐ googlesamples.github.io/android-custom-lint-rules
Lint UAST dump¶
public class FooDetector : Detector(), SourceCodeScanner {
override fun getApplicableMethodNames() = listOf("foo")
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) =
println(node.asRecursiveLogString())
}
fun foo(bar: String, baz: Int) = Unit
foo(bar = "BAR", baz = 42)
UCallExpression (kind = UastCallKind(name='method_call'), argCount = 2))
UIdentifier (Identifier (foo))
USimpleNameReferenceExpression (identifier = foo, resolvesTo = null)
ULiteralExpression (value = "BAR")
ULiteralExpression (value = 42)
List AOSP ATD devices¶
"$ANDROID_HOME"/cmdline-tools/latest/bin/sdkmanager --list --channel=3 | grep atd | cut -d ' ' -f 3
system-images;android-30;aosp_atd;arm64-v8a
system-images;android-30;aosp_atd;x86
system-images;android-30;aosp_atd;x86_64
system-images;android-30;google_atd;arm64-v8a
system-images;android-30;google_atd;x86
system-images;android-30;google_atd;x86_64
system-images;android-31;aosp_atd;x86_64
system-images;android-31;google_atd;x86_64
List resources at runtime¶
/* Colors */
R.color::class.java.fields.forEach {
val name = it.name
val colorRes /*@ColorRes*/ = it.getInt(null)
val colorInt /*@ColorInt*/ = ContextCompat.getColor(this, colorRes)
}
/* Drawables */
R.drawable::class.java.fields.forEach {
val name = it.name
val drawableRes /*@DrawableRes*/ = it.getInt(null)
val drawable = ContextCompat.getDrawable(this, drawableRes)
}
/* Strings */
R.string::class.java.fields.forEach {
val name = it.name
val stringRes /*@StringRes*/ = it.getInt(null)
val string = getString(stringRes)
}
LiveData Events¶
Previously known as SingleLiveEvent
๐
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T : Any>(private val content: T) {
var hasBeenHandled = false
private set
/**
* Returns the [content], even if it's already been handled.
*/
fun peek(): T = content
/**
* Returns the [content] and prevents its use again.
*/
fun getIfNotHandled(): T? = if (hasBeenHandled) null else content.also { hasBeenHandled = true }
/**
* Executes the [block] if the [content] has not already been handled.
*/
fun ifNotHandled(block: (T) -> Unit) = getIfNotHandled()?.let(block)
}
/**
* An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s value has already been handled.
*
* [onEvent] is *only* called if the [Event]'s contents has not been handled.
*/
class EventObserver<T : Any>(
private val onEvent: (T) -> Unit,
) : Observer<Event<T>> {
override fun onChanged(event: Event<T>) {
event.ifNotHandled(onEvent)
}
}
fun <T : Any> LiveData<Event<T>>.observeEvent(
owner: LifecycleOwner,
onEvent: (T) -> Unit,
) = observe(owner, EventObserver(onEvent))
LiveData observe non-null data¶
fun <T> LiveData<T>.observeNotNull(
owner: LifecycleOwner,
observer: (data: T & Any) -> Unit,
) = observe(owner) { it?.let(observer) }
LiveData unit testing¶
/**
* Gets the value of a [LiveData] or waits for it to have one, with a timeout.
*
* Use this extension from host-side (JVM) tests. It's recommended to use it alongside
* `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
*/
fun <T> LiveData<T>.awaitValue(
duration: Duration = 2.seconds,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
removeObserver(this)
}
}
observeForever(observer)
afterObserve.invoke()
if (!latch.await(duration.inWholeMilliseconds, MILLISECONDS)) {
removeObserver(observer)
throw TimeoutException("LiveData value was never set.")
}
@Suppress("UNCHECKED_CAST")
return data as T
}
/**
* Observes a [LiveData] until the `block` is done executing.
*/
suspend fun <T> LiveData<T>.test(block: suspend () -> Unit) {
val observer = Observer<T> { }
try {
observeForever(observer)
block()
} finally {
removeObserver(observer)
}
}
Locate APK files¶
adb shell pm list packages -f
adb shell pm path <package-name>
Manually sign APK¶
apksigner sign --ks debug.keystore --key-pass pass:android --ks-key-alias androiddebugkey --ks-pass pass:android app-release.apk
Mutliplatform Parcelize plugin¶
Kotlin's Parcelize plugin works only on Android project. Wrapping it in a Kotlin Multiplatform project allows us to use it in JVM projects, without leaking Android implementation details.
plugins {
kotlin("multiplatform")
id("com.android.library")
id("kotlin-parcelize")
}
android { /* ... */ }
kotlin {
android()
jvm()
sourceSets {
val commonMain by getting { /* ... */ }
val commonTest by getting { /* ... */ }
val jvmMain by getting { /* ... */ }
val jvmTest by getting { /* ... */ }
val androidMain by getting { /* ... */ }
val androidUnitTest by getting { /* ... */ }
val androidInstrumentedTest by getting { /* ... */ }
}
}
expect class Parcel {
fun writeLong(long: Long)
fun readLong(): Long
}
expect interface Parcelable
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
expect annotation class Parcelize()
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Repeatable
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
expect annotation class TypeParceler<T, P : Parceler<in T>>
expect interface Parceler<T> {
fun create(parcel: Parcel): T
fun T.write(parcel: Parcel, flags: Int)
}
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
expect annotation class IgnoredOnParcel()
actual typealias Parcel = android.os.Parcel
actual typealias Parcelable = android.os.Parcelable
actual typealias Parcelize = kotlinx.parcelize.Parcelize
actual typealias TypeParceler<T, P> = kotlinx.parcelize.TypeParceler<T, P>
actual typealias Parceler<T> = kotlinx.parcelize.Parceler<T>
actual typealias IgnoredOnParcel = kotlinx.parcelize.IgnoredOnParcel
actual class Parcel {
actual fun writeLong(long: Long): Unit = TODO()
actual fun readLong(): Long = TODO()
}
actual interface Parcelable
actual annotation class Parcelize
actual annotation class TypeParceler<T, P : Parceler<in T>>
actual interface Parceler<T> {
actual fun create(parcel: Parcel): T
actual fun T.write(parcel: Parcel, flags: Int)
}
actual annotation class IgnoredOnParcel
Parcel extensions¶
fun <T> Parcel.use(block: (Parcel) -> T): T = try { block(this) } finally { recycle() }
context(Parceler<T>)
fun <T> T.parcelize(): T = Parcel.obtain().use {
write(it, 0)
it.setDataPosition(0)
create(it)
}
inline fun <reified R : Parcelable> R.marshall(): ByteArray = Parcel.obtain().use {
it.writeValue(this)
it.marshall()
}
inline fun <reified R : Parcelable> ByteArray.unmarshall(): R? = Parcel.obtain().use {
it.unmarshall(this, 0, size)
it.setDataPosition(0)
it.readValue(R::class.java.classLoader) as T
}
//region Read boxed primitive values
@SuppressLint("ParcelClassLoader")
fun Parcel.readIntValue(): Int? = readValue(null) as Int?
@SuppressLint("ParcelClassLoader")
fun Parcel.readLongValue(): Long? = readValue(null) as Long?
@SuppressLint("ParcelClassLoader")
fun Parcel.readFloatValue(): Float? = readValue(null) as Float?
@SuppressLint("ParcelClassLoader")
fun Parcel.readDoubleValue(): Double? = readValue(null) as Double?
@SuppressLint("ParcelClassLoader")
fun Parcel.readBooleanValue(): Boolean? = readValue(null) as Boolean?
//endregion
/**
* This will check an [Int] flag to switch between `null` and non-`null` value:
* - `0` means the value is `null` and nothing must be read
* - `1` means the value is non-`null` and can be read
*/
inline fun <T> Parcel.readNullable(reader: Parcel.() -> T) =
if (readInt() != 0) reader() else null
/**
* This will write an [Int] flag to switch between `null` and non-`null` value:
* - `0` when the value is `null`
* - `1` when the value is non-`null`
*/
inline fun <T> Parcel.writeNullable(value: T?, writer: Parcel.(value: T) -> Unit) {
if (value == null) return writeInt(0)
writeInt(1)
writer(value)
}
Parcelize and TypeParceler¶
class Foo
object FooParceler : Parceler<Foo?> {
override fun create(parcel: Parcel): Foo? = TODO()
override fun Foo?.write(parcel: Parcel, flags: Int) = TODO()
}
// typealias helpers
typealias FooTypeParceler = TypeParceler<Foo, FooParceler>
typealias FooNullableTypeParceler = TypeParceler<Foo?, FooParceler>
@Parcelize
@FooTypeParceler
@FooNullableTypeParceler
class Data(val foo: Foo, val nullableFoo: Foo?) : Parcelable
Print APK certificates¶
find . -name '*.apk' -type f -exec echo "APK: {}" \; -exec keytool -printcert -jarfile "{}" \;
Print keystore entries¶
keytool -list -v -keystore ~/.android/debug.keystore
Keystore type: PKCS12
Keystore provider: SUN
Your keystore contains 1 entry
Alias name: androiddebugkey
Creation date: 24 Aug 2021
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: C=US, O=Android, CN=Android Debug
Issuer: C=US, O=Android, CN=Android Debug
Serial number: 1
Valid from: Tue Aug 24 14:21:42 CEST 2021 until: Thu Aug 17 14:21:42 CEST 2051
Certificate fingerprints:
SHA1: 9C:2B:0E:AD:D2:12:ED:68:42:00:F5:41:4A:29:A8:10:FD:4F:94:62
SHA256: 35:21:3A:55:E0:8E:2F:98:34:B6:B1:0B:13:DD:2B:25:A7:E5:9E:39:85:8C:4E:C3:F1:82:CD:33:2E:91:57:4D
Signature algorithm name: SHA1withRSA (weak)
Subject Public Key Algorithm: 2048-bit RSA key
Version: 1
Project view by default¶
Help
โ Edit Custom Properties
โ studio.projectview=true
R8 flags¶
com.android.tools.r8.printtimes
com.android.tools.r8.printmemory
Recursive View children sequence¶
fun View.recursiveChildren(): Sequence<View> = sequence {
yield(this@recursiveChildren)
if (this@recursiveChildren !is ViewGroup) return@sequence
children.forEach { yieldAll(it.recursiveChildren()) }
}
Resource identifier for MATCH_PARENT and WRAP_CONTENT¶
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- ViewGroup.LayoutParams.MATCH_PARENT -->
<item name="match_parent" type="dimen" format="integer" tools:ignore="ResourceName">-1</item>
<!-- ViewGroup.LayoutParams.WRAP_CONTENT -->
<item name="wrap_content" type="dimen" format="integer" tools:ignore="ResourceName">-2</item>
</resources>
Resources abstraction¶
import android.content.res.Resources
import androidx.annotation.BoolRes
sealed class BooleanResource {
companion object {
fun from(boolean: Boolean): BooleanResource = BooleanValueResource(boolean)
fun fromColorId(@BoolRes id: Int): BooleanResource = BooleanIdResource(id)
}
}
private data class BooleanValueResource(val value: Boolean) : BooleanResource()
private data class BooleanIdResource(@BoolRes val id: Int) : BooleanResource()
fun BooleanResource.toBoolean(resources: Resources): Boolean = when (this) {
is BooleanValueResource -> value
is BooleanIdResource -> resources.getBoolean(id)
}
import android.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.Color
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.core.content.res.ResourcesCompat
sealed class ColorResource {
companion object {
fun from(color: String): ColorResource = ColorStringResource(color)
fun fromColorId(@ColorRes id: Int): ColorResource = ColorIdResource(id)
}
}
private data class ColorStringResource(val value: String) : ColorResource()
private data class ColorIdResource(@ColorRes val id: Int) : ColorResource()
@ColorInt
fun ColorResource.toColorInt(resources: Resources, theme: Resources.Theme? = null): Int = when (this) {
is ColorStringResource -> Color.parseColor(value)
is ColorIdResource -> ResourcesCompat.getColor(resources, id, theme)
}
fun ColorResource.toColorStateList(resources: Resources, theme: Resources.Theme? = null): ColorStateList? = when (this) {
is ColorStringResource -> ColorStateList.valueOf(toColorInt(resources))
is ColorIdResource -> ResourcesCompat.getColorStateList(resources, id, theme)
}
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toDrawable
sealed class DrawableResource {
companion object {
fun from(drawable: Drawable): DrawableResource = DrawableValueResource(drawable)
fun fromBitmap(bitmap: Bitmap): DrawableResource = DrawableBitmapResource(bitmap)
fun fromDrawableId(@DrawableRes id: Int): DrawableResource = DrawableIdResource(id)
}
}
private data class DrawableValueResource(val drawable: Drawable) : DrawableResource()
private data class DrawableBitmapResource(val bitmap: Bitmap) : DrawableResource()
private data class DrawableIdResource(@DrawableRes val id: Int) : DrawableResource()
fun DrawableResource.toDrawable(resources: Resources, theme: Resources.Theme? = null): Drawable? = when (this) {
is DrawableValueResource -> drawable
is DrawableBitmapResource -> bitmap.toDrawable(resources)
is DrawableIdResource -> ResourcesCompat.getDrawable(resources, id, theme)
}
import android.content.res.Resources
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import java.text.MessageFormat
sealed class TextResource {
companion object {
fun none(): TextResource = NullTextResource
fun fromString(text: CharSequence): TextResource = StringTextResource(text)
fun fromStringId(@StringRes id: Int, vararg args: Any = emptyArray()): TextResource = StringIdTextResource(id, args.toList())
fun fromPluralId(@PluralsRes id: Int, quantity: Int, vararg args: Any = emptyArray()): TextResource = PluralIdTextResource(id, quantity, args.toList())
fun fromMessage(@StringRes id: Int, vararg args: Any = emptyArray()): TextResource = MessageIdTextResource(id, args.toList())
}
@Deprecated(
message = "Suspicious toString() usage, please use toString(resources) if you need to resolve the TextResource.",
replaceWith = ReplaceWith("toString(resources)"),
level = DeprecationLevel.WARNING
)
override fun toString(): String = super.toString()
}
private object NullTextResource : TextResource()
private data class StringTextResource(val text: CharSequence) : TextResource()
private data class StringIdTextResource(@StringRes val id: Int, val args: List<Any>) : TextResource()
private data class PluralIdTextResource(@PluralsRes val id: Int, val quantity: Int, val args: List<Any>) : TextResource()
private data class MessageIdTextResource(@StringRes val id: Int, val args: List<Any>) : TextResource()
fun TextResource?.orNone(): TextResource = this ?: TextResource.none()
fun TextResource.toText(resources: Resources): CharSequence? = when (this) {
NullTextResource -> null
is StringTextResource -> text
is StringIdTextResource -> if (args.isEmpty()) resources.getText(id) else toString(resources)
is PluralIdTextResource -> if (args.isEmpty()) resources.getQuantityText(id, quantity) else toString(resources)
is MessageIdTextResource -> if (args.isEmpty()) MessageFormat.format(resources.getString(id)) else toString(resources)
}
fun TextResource.toString(resources: Resources): String? = when (this) {
NullTextResource -> null
is StringTextResource -> text.toString()
is StringIdTextResource -> if (args.isEmpty()) resources.getString(id) else resources.getString(id, *args.mapTextResourceToString(resources))
is PluralIdTextResource -> if (args.isEmpty()) resources.getQuantityString(id, quantity) else resources.getQuantityString(id, quantity, *args.mapTextResourceToString(resources))
is MessageIdTextResource -> if (args.isEmpty()) MessageFormat.format(resources.getString(id)) else MessageFormat.format(resources.getString(id), *args.mapTextResourceToString(resources))
}
private fun <E> List<E>.mapTextResourceToString(resources: Resources) = map { if (it is TextResource) it.toString(resources) else it }.toTypedArray()
Robolectric fake Browser Activity¶
import org.robolectric.RuntimeEnvironment.getApplication
import org.robolectric.Shadows.shadowOf
fun addFakeWebBrowserActivity() = shadowOf(getApplication().packageManager).apply {
with(ComponentName(getApplication().packageName, "FakeBrowserActivity")) {
addActivityIfNotPresent(this)
addIntentFilterForActivity(
this,
IntentFilter(Intent.ACTION_VIEW).apply {
addDataScheme("https")
addCategory(Intent.CATEGORY_DEFAULT)
addCategory(Intent.CATEGORY_BROWSABLE)
},
)
}
}
Run command as a specific application user-id¶
adb shell run-as <package-name> <command> [<args>]
screencap¶
adb shell screencap -p "/sdcard/screencap.png" && adb pull "/sdcard/screencap.png" "$USER/Desktop/%time::=-%%random%.png" && adb shell rm "/sdcard/screencap.png"
%comspec% /c adb shell screencap -p "/sdcard/screencap.png" && adb pull "/sdcard/screencap.png" "%UserProfile%\Desktop\%time::=-%%random%.png" && adb shell rm "/sdcard/screencap.png"
scrcpy¶
-
record
scrcpy -m 1080 --show-touches --no-display --record "$USER/Desktop/%time::=-%%random%.mp4"
- mirror%comspec% /c scrcpy -m 1080 --show-touches --no-display --record "%UserProfile%\Desktop\%time::=-%%random%.mp4"
scrcpy -m 1080 --show-touches
%comspec% /c scrcpy -m 1080 --show-touches
Step by step installation¶
# Installing OpenJDK
# https://openjdk.org/install/
sudo apt-get update
sudo apt-get install openjdk-17-jdk
# Installing Android Command Line Tools
# https://developer.android.com/studio#command-line-tools-only
cd ~
curl https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -o /tmp/cmd-tools.zip
mkdir -p android/cmdline-tools
unzip -q -d android/cmdline-tools /tmp/cmd-tools.zip
mv android/cmdline-tools/cmdline-tools android/cmdline-tools/latest
rm /tmp/cmd-tools.zip
# Setting up environment variables
export ANDROID_HOME=$HOME/android
export ANDROID_SDK_ROOT=${ANDROID_HOME}
export PATH=${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:${PATH}
# Accepting SDK licenses
yes | sdkmanager --licenses
# Installing SDK components
sdkmanager --update
sdkmanager --list
sdkmanager --list_installed
sdkmanager --install "platforms;android-33"
Strict Modes¶
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyFlashScreen()
.penaltyLog()
.build()
.let(StrictMode::setThreadPolicy)
StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
.let(StrictMode::setVmPolicy)
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.detectFragmentReuse()
.detectFragmentTagUsage()
.detectRetainInstanceUsage()
.detectTargetFragmentUsage()
.detectWrongFragmentContainer()
.detectSetUserVisibleHint()
.penaltyLog().build()
Suppress unsupported options¶
android.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarnings,\
android.enableR8.fullMode,\
android.enableUnitTestBinaryResources,\
...
Tools sample resources as JSON files¶
{
"data": [
{
"name": "Mike Barrett",
"address": "4898 Locust Rd",
"avatar": "@sample/avatars"
},
{
"name": "Kylie Long",
"address": "2659 Taylor St",
"avatar": "@sample/avatars"
}
]
}
<ImageView
android:id="@+id/avatar"
tools:src="@sample/users.json/data/avatar" />
<TextView
android:id="@+id/name"
tools:text="@sample/users.json/data/name" />
<TextView
android:id="@+id/address"
tools:text="@sample/users.json/data/address" />
Uninstall apps by package name¶
adb shell pm list packages `: # List packages` |
cut -d ':' -f2 `: # Extract package name` |
grep ^com. `: # Optional pattern` |
xargs --verbose -n1 adb uninstall
Or with fzf
adb shell pm list packages `: # List packages` |
cut -d ':' -f2 `: # Extract package name` |
fzf --header '๐ฆ Select one or more packages to uninstall' --border --reverse --multi |
xargs --no-run-if-empty --verbose -n1 adb uninstall
USSD and secret codes¶
These codes must be typed in the dialer app.
*#06#
: IMEI*#*#4636#*#*
: Device info*#*#7780#*#*
: Factory reset โป๏ธ
I've created Secret Codes to help you find theses codes directly on your device.
ViewBinding extension¶
fun <Binding : ViewBinding> AppCompatActivity.setContentView(
inflate: (LayoutInflater) -> Binding
): Binding = inflate(layoutInflater).also {
setContentView(it.root)
}
private lateinit var binding: MainViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = setContentView(MainViewBinding::inflate)
}
ViewBinding one-liner¶
private lateinit var binding: MainViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(MainViewBinding.inflate(layoutInflater).also { binding = it }.root)
}
ViewModel in custom View¶
class CustomView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
private val viewModel by lazy { ViewModelProvider(findViewTreeViewModelStoreOwner()!!).get<CustomViewModel>() }
override fun onAttachedToWindow() {
super.onAttachedToWindow()
viewModel.state.observe(findViewTreeLifecycleOwner()!!) { TODO() }
}
}
WebView Safe Browsing¶
<manifest>
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" android:value="true" />
<!-- ... -->
</manifest>
WorkManager diagnostic¶
๐ This provides information on:
- Work requests that were completed in the last 24 hours.
- Work requests that are currently running.
- Scheduled work requests.
adb shell am broadcast -a "androidx.work.diagnostics.REQUEST_DIAGNOSTICS" -p my.package.name