How to recognise that the user has
just uninstalled your Android app
fb.me/pjakubczyk
+AleksanderPiotrowski
@pelotasplus
Opera Max
The Java way
Read the broadcast
<receiver android:name=".PackageWatcher">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED"/>
<action android:name="android.intent.action.PACKAGE_REMOVED"/>
<action android:name="android.intent.action.PACKAGE_REPLACED"
/>
<data android:scheme="package"/>
</intent-filter>
</receiver>
Read the broadcast
void onReceive(Context context, Intent intent) {
Bundle bundle = intent.getExtras();
Iterator<String> it = bundle.keySet().iterator;
while (it.hasNext()) {
String key = it.next();
Log.e("DDD", key +"="+bundle.get(key)); }
Usually we see (install)
E/DDD (29199): Dumping Intent start
[android.intent.extra.UID=10089]
[android.intent.extra.user_handle=0]
E/DDD (29199): Dumping Intent end
Usually we see (reinstall)
E/DDD (29199): Dumping Intent start
[android.intent.extra.REMOVED_FOR_ALL_USERS=false]
[android.intent.extra.UID=10089]
[android.intent.extra.DATA_REMOVED=false]
[android.intent.extra.REPLACING=true]
[android.intent.extra.user_handle=0]
E/DDD (29199): Dumping Intent end
Usually we see (uninstall)
E/DDD (29199): Dumping Intent start
[android.intent.extra.REMOVED_FOR_ALL_USERS=true]
[android.intent.extra.UID=10089]
[android.intent.extra.DATA_REMOVED=true]
[android.intent.extra.user_handle=0]
E/DDD (29199): Dumping Intent end
Let’s uninstall our app
and there’s nothing ….
Why ?
OS unregisters listener during removal
What Opera does?
It does not listen for package removal
it does some magic ;-)
… not in Java code
Getting the APK
Getting the APK
● genymotion with
gapps installed
● get app from play store
● be careful with the right ABI
Getting the APK
1. adb shell
2. pm list packages
Getting the APK
3. pm path com.opera.max
4. adb pull /data/app/com.
opera.max.apk
Hacking APK
Apktool
A tool for reverse engineering
Android apk files
Made with <3 in Poland ;-)
Apktool
Easy to use
$ apktool d com.opera.max.
apk
Apktool
● decoded XML files
● smali assembly code
● PNGs, layouts, resources
● id-s mapping
with Opera Max APK
live apktool demo
Opera Findings
Found a clue!
There are *.so files
We can inspect them to see more
Tools: strings, objdump, nm, readelf
rudy$ strings opera/lib/armeabi/libuo.so (II)
...
inotify_init
inotify_add_watch
inotify_rm_watch
/data/data/%s/
%s%s
inotify framework
http://linux.die.net/man/7/inotify
The inotify API provides a mechanism
for monitoring file system events.
Inotify can be used to monitor individual
files, or to monitor directories.
rudy$ strings opera/lib/armeabi/libuo.so (I)
...
Android
start
android.intent.action.VIEW
--user
...
am command
part of Android system
/system/bin/am
A way to start apps, intents and
whatnot
more details
$ ps
USER PID PPID
u0_a91 24318 20265 246900 27716 ffffffff b6edf5cc S
com.opera.max
u0_a91 24337 24318 856 336 c00e4944 b6f72158 S
/data/app-lib/com.opera.max-2/libuo.so
The scenario
1. Fork the native process
2. Inside the child process use inotify to watch
a file
3. Watcher is woken up on file deletion. Start
another native process
4. The last process run the ‘am’
(ActivityManager) command to run intent.
Setup
JNI
local.properties
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System,
please read the
sdk.dir=/Users/alek/android-sdk
ndk.dir=/Users/alek/android-ndk-r10e
build.gradle
android.defaultConfig {
applicationId "pl.pelotasplus.actionafteruninstall"
ndk {
moduleName "hello-jni"
ldLibs "log", "android"
stl "stlport_static"
}
}
MainActivity.java declaring
public class MainActivity extends AppCompatActivity {
public native String stringFromJNI();
public native void observer();
static {
System.loadLibrary("hello-jni");
// System.loadLibrary("/data/data/com.foo.test/lib/liba.so");
}
}
MainActivity.java calling
protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.textView);
textView.setText(stringFromJNI());
observer();
}
project structure
Native code
JNI
Sample by Google
jstring
Java_pl_pelotasplus_actionafteruninstall_MainActivity_stringFro
mJNI
(JNIEnv* env, jobject thiz)
{
return (*env)->NewStringUTF(
env,
"Hello from JNI ! Compiled with ABI foo."
);
}
Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := hello-jni.c
LOCAL_LDFLAGS += -llog -lpthread
include $(BUILD_SHARED_LIBRARY)
Application.mk
APP_ABI := armeabi-v7a
# all
APP_STL := stlport_static
inotify on Linux
int main( int argc, char **argv) {
int length, i = 0;
int fd;
int wd;
char buffer[BUF_LEN];
fd = inotify_init();
printf("fd=%dn", fd);
}
inotify on Linux
int main( int argc, char **argv)
{
[...]
wd = inotify_add_watch(fd, "/var/tmp",
IN_MODIFY | IN_CREATE | IN_DELETE);
length = read( fd, buffer, BUF_LEN );
printf("length=%dn", length);
if (length < 0) {
perror("read");
}
inotify on Linux
while (i < length) {
struct inotify_event *event = (struct inotify_event*)&buffer[ i];
printf("Event len %dn", event->len);
if (event->len) {
if (event->mask & IN_DELETE) {
if (event->mask & IN_ISDIR) {
printf( "The directory %s was deleted.n", event->name );
} else {
printf( "The file %s was deleted.n", event->name );
inotify on Android (pseudo code)
void observer(void) {
inotify_init();
inotify_add_watch(fd, DIRECTORY, IN_DELETE);
if (event->mask & IN_DELETE) {
startIntent();
}
}
first attempt
void
Java_pl_pelotasplus_actionafteruninstall_MainActivity_observer(JNIEnv* env, jobject
thiz)
{
observer();
}
App blocked as native code blocked app
second attempt, with thread
void
Java_pl_pelotasplus_actionafteruninstall_MainActivity_observer
(JNIEnv* env, jobject thiz)
{
pthread_attr_init(&attr);
pthread_create(&thread, &attr, &observer_thread, NULL);
}
App not blocked but native code stopped when stopping app for
uninstalling
third attempt, with fork
void
Java_pl_pelotasplus_actionafteruninstall_MainActivity_observer(JNIEnv* env, jobject thiz)
{
pid_t pid;
pid = fork();
if (pid == 0) {
__android_log_print(ANDROID_LOG_INFO, TAG, "Fork childn");
observer();
}
}
start intent, another fork
void startIntent(void) {
pid_t p = fork();
if (p == 0) {
__android_log_print(ANDROID_LOG_INFO, TAG, "startIntent %d", getpid());
system("/system/bin/am start --user 0 -a android.intent.
action.VIEW -d http://droidcon.de");
}
}
Live demo of our app
https://github.
com/pelotasplus/A
ctionAfterUninstall
Check the dirty source code
Moral
> What happens when I call fork() in JNI code? Will this totally break the
> Activity lifecycle model in Android?
Don't do this. Just don't.
--
Dianne Hackborn
Android framework engineer
hack...@android.com
http://markmail.org/message/ruqp2t6gvhnhv654

How to recognise that the user has just uninstalled your app

  • 1.
    How to recognisethat the user has just uninstalled your Android app fb.me/pjakubczyk +AleksanderPiotrowski @pelotasplus
  • 2.
  • 4.
  • 5.
    Read the broadcast <receiverandroid:name=".PackageWatcher"> <intent-filter> <action android:name="android.intent.action.PACKAGE_ADDED"/> <action android:name="android.intent.action.PACKAGE_REMOVED"/> <action android:name="android.intent.action.PACKAGE_REPLACED" /> <data android:scheme="package"/> </intent-filter> </receiver>
  • 6.
    Read the broadcast voidonReceive(Context context, Intent intent) { Bundle bundle = intent.getExtras(); Iterator<String> it = bundle.keySet().iterator; while (it.hasNext()) { String key = it.next(); Log.e("DDD", key +"="+bundle.get(key)); }
  • 7.
    Usually we see(install) E/DDD (29199): Dumping Intent start [android.intent.extra.UID=10089] [android.intent.extra.user_handle=0] E/DDD (29199): Dumping Intent end
  • 8.
    Usually we see(reinstall) E/DDD (29199): Dumping Intent start [android.intent.extra.REMOVED_FOR_ALL_USERS=false] [android.intent.extra.UID=10089] [android.intent.extra.DATA_REMOVED=false] [android.intent.extra.REPLACING=true] [android.intent.extra.user_handle=0] E/DDD (29199): Dumping Intent end
  • 9.
    Usually we see(uninstall) E/DDD (29199): Dumping Intent start [android.intent.extra.REMOVED_FOR_ALL_USERS=true] [android.intent.extra.UID=10089] [android.intent.extra.DATA_REMOVED=true] [android.intent.extra.user_handle=0] E/DDD (29199): Dumping Intent end
  • 10.
    Let’s uninstall ourapp and there’s nothing …. Why ? OS unregisters listener during removal
  • 11.
    What Opera does? Itdoes not listen for package removal it does some magic ;-) … not in Java code
  • 12.
  • 13.
    Getting the APK ●genymotion with gapps installed ● get app from play store ● be careful with the right ABI
  • 14.
    Getting the APK 1.adb shell 2. pm list packages
  • 15.
    Getting the APK 3.pm path com.opera.max 4. adb pull /data/app/com. opera.max.apk
  • 16.
  • 17.
    Apktool A tool forreverse engineering Android apk files Made with <3 in Poland ;-)
  • 18.
    Apktool Easy to use $apktool d com.opera.max. apk
  • 19.
    Apktool ● decoded XMLfiles ● smali assembly code ● PNGs, layouts, resources ● id-s mapping
  • 20.
    with Opera MaxAPK live apktool demo
  • 21.
  • 22.
    Found a clue! Thereare *.so files We can inspect them to see more Tools: strings, objdump, nm, readelf
  • 23.
    rudy$ strings opera/lib/armeabi/libuo.so(II) ... inotify_init inotify_add_watch inotify_rm_watch /data/data/%s/ %s%s
  • 24.
    inotify framework http://linux.die.net/man/7/inotify The inotifyAPI provides a mechanism for monitoring file system events. Inotify can be used to monitor individual files, or to monitor directories.
  • 25.
    rudy$ strings opera/lib/armeabi/libuo.so(I) ... Android start android.intent.action.VIEW --user ...
  • 26.
    am command part ofAndroid system /system/bin/am A way to start apps, intents and whatnot
  • 27.
    more details $ ps USERPID PPID u0_a91 24318 20265 246900 27716 ffffffff b6edf5cc S com.opera.max u0_a91 24337 24318 856 336 c00e4944 b6f72158 S /data/app-lib/com.opera.max-2/libuo.so
  • 28.
    The scenario 1. Forkthe native process 2. Inside the child process use inotify to watch a file 3. Watcher is woken up on file deletion. Start another native process 4. The last process run the ‘am’ (ActivityManager) command to run intent.
  • 29.
  • 30.
    local.properties # Location ofthe SDK. This is only used by Gradle. # For customization when using a Version Control System, please read the sdk.dir=/Users/alek/android-sdk ndk.dir=/Users/alek/android-ndk-r10e
  • 31.
    build.gradle android.defaultConfig { applicationId "pl.pelotasplus.actionafteruninstall" ndk{ moduleName "hello-jni" ldLibs "log", "android" stl "stlport_static" } }
  • 32.
    MainActivity.java declaring public classMainActivity extends AppCompatActivity { public native String stringFromJNI(); public native void observer(); static { System.loadLibrary("hello-jni"); // System.loadLibrary("/data/data/com.foo.test/lib/liba.so"); } }
  • 33.
    MainActivity.java calling protected voidonCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.textView); textView.setText(stringFromJNI()); observer(); }
  • 34.
  • 35.
  • 36.
    Sample by Google jstring Java_pl_pelotasplus_actionafteruninstall_MainActivity_stringFro mJNI (JNIEnv*env, jobject thiz) { return (*env)->NewStringUTF( env, "Hello from JNI ! Compiled with ABI foo." ); }
  • 37.
    Android.mk LOCAL_PATH := $(callmy-dir) include $(CLEAR_VARS) LOCAL_MODULE := hello-jni LOCAL_SRC_FILES := hello-jni.c LOCAL_LDFLAGS += -llog -lpthread include $(BUILD_SHARED_LIBRARY)
  • 38.
    Application.mk APP_ABI := armeabi-v7a #all APP_STL := stlport_static
  • 39.
    inotify on Linux intmain( int argc, char **argv) { int length, i = 0; int fd; int wd; char buffer[BUF_LEN]; fd = inotify_init(); printf("fd=%dn", fd); }
  • 40.
    inotify on Linux intmain( int argc, char **argv) { [...] wd = inotify_add_watch(fd, "/var/tmp", IN_MODIFY | IN_CREATE | IN_DELETE); length = read( fd, buffer, BUF_LEN ); printf("length=%dn", length); if (length < 0) { perror("read"); }
  • 41.
    inotify on Linux while(i < length) { struct inotify_event *event = (struct inotify_event*)&buffer[ i]; printf("Event len %dn", event->len); if (event->len) { if (event->mask & IN_DELETE) { if (event->mask & IN_ISDIR) { printf( "The directory %s was deleted.n", event->name ); } else { printf( "The file %s was deleted.n", event->name );
  • 42.
    inotify on Android(pseudo code) void observer(void) { inotify_init(); inotify_add_watch(fd, DIRECTORY, IN_DELETE); if (event->mask & IN_DELETE) { startIntent(); } }
  • 43.
    first attempt void Java_pl_pelotasplus_actionafteruninstall_MainActivity_observer(JNIEnv* env,jobject thiz) { observer(); } App blocked as native code blocked app
  • 44.
    second attempt, withthread void Java_pl_pelotasplus_actionafteruninstall_MainActivity_observer (JNIEnv* env, jobject thiz) { pthread_attr_init(&attr); pthread_create(&thread, &attr, &observer_thread, NULL); } App not blocked but native code stopped when stopping app for uninstalling
  • 45.
    third attempt, withfork void Java_pl_pelotasplus_actionafteruninstall_MainActivity_observer(JNIEnv* env, jobject thiz) { pid_t pid; pid = fork(); if (pid == 0) { __android_log_print(ANDROID_LOG_INFO, TAG, "Fork childn"); observer(); } }
  • 46.
    start intent, anotherfork void startIntent(void) { pid_t p = fork(); if (p == 0) { __android_log_print(ANDROID_LOG_INFO, TAG, "startIntent %d", getpid()); system("/system/bin/am start --user 0 -a android.intent. action.VIEW -d http://droidcon.de"); } }
  • 47.
    Live demo ofour app
  • 48.
  • 49.
    Moral > What happenswhen I call fork() in JNI code? Will this totally break the > Activity lifecycle model in Android? Don't do this. Just don't. -- Dianne Hackborn Android framework engineer hack...@android.com http://markmail.org/message/ruqp2t6gvhnhv654