From 9c9eb48bf03820d21b0d51a620c0320a5ae35653 Mon Sep 17 00:00:00 2001
From: Simon Fels <morphis@gravedo.de>
Date: Tue, 13 Dec 2016 18:42:50 +0100
Subject: [PATCH] Implement simple activity to show all launchable applications

---
 android/appmgr/AndroidManifest.xml            |   8 +-
 android/appmgr/res/layout/app_view.xml        |  17 +
 .../appmgr/res/layout/list_item_icon_text.xml |  38 ++
 android/appmgr/res/values/dimens.xml          |   5 +
 .../src/org/anbox/appmgr/AppListAdapter.java  |  89 ++++
 .../src/org/anbox/appmgr/AppListFragment.java |  74 ++++
 .../appmgr/src/org/anbox/appmgr/AppModel.java | 101 +++++
 .../src/org/anbox/appmgr/AppViewActivity.java |  41 ++
 .../org/anbox/appmgr/AppsGridFragment.java    |  90 ++++
 .../src/org/anbox/appmgr/AppsLoader.java      | 184 ++++++++
 .../src/org/anbox/appmgr/GridFragment.java    | 399 ++++++++++++++++++
 .../anbox/appmgr/PackageIntentReceiver.java   |  62 +++
 12 files changed, 1106 insertions(+), 2 deletions(-)
 create mode 100644 android/appmgr/res/layout/app_view.xml
 create mode 100644 android/appmgr/res/layout/list_item_icon_text.xml
 create mode 100644 android/appmgr/res/values/dimens.xml
 create mode 100644 android/appmgr/src/org/anbox/appmgr/AppListAdapter.java
 create mode 100644 android/appmgr/src/org/anbox/appmgr/AppListFragment.java
 create mode 100644 android/appmgr/src/org/anbox/appmgr/AppModel.java
 create mode 100644 android/appmgr/src/org/anbox/appmgr/AppViewActivity.java
 create mode 100644 android/appmgr/src/org/anbox/appmgr/AppsGridFragment.java
 create mode 100644 android/appmgr/src/org/anbox/appmgr/AppsLoader.java
 create mode 100644 android/appmgr/src/org/anbox/appmgr/GridFragment.java
 create mode 100644 android/appmgr/src/org/anbox/appmgr/PackageIntentReceiver.java

diff --git a/android/appmgr/AndroidManifest.xml b/android/appmgr/AndroidManifest.xml
index 7ffbb79c..6b9277b6 100644
--- a/android/appmgr/AndroidManifest.xml
+++ b/android/appmgr/AndroidManifest.xml
@@ -6,14 +6,18 @@
     <application
         android:name="org.anbox.appmgr.MainApplication">
         <activity
-            android:name=".LauncherActivity"
-            android:launchMode="singleTask">
+            android:name=".LauncherActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.HOME"/>
                 <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
+        <activity
+            android:name=".AppViewActivity"
+            android:label="@string/app_name"
+            android:excludeFromRecents="true">
+        </activity>
         <service
             android:name=".LauncherService"
             android:exported="false">
diff --git a/android/appmgr/res/layout/app_view.xml b/android/appmgr/res/layout/app_view.xml
new file mode 100644
index 00000000..1b76962f
--- /dev/null
+++ b/android/appmgr/res/layout/app_view.xml
@@ -0,0 +1,17 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    tools:context=".AppViewActivity">
+
+    <fragment
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:name="org.anbox.appmgr.AppsGridFragment"
+            android:id="@+id/apps_grid" />
+
+</RelativeLayout>
diff --git a/android/appmgr/res/layout/list_item_icon_text.xml b/android/appmgr/res/layout/list_item_icon_text.xml
new file mode 100644
index 00000000..9ea839c4
--- /dev/null
+++ b/android/appmgr/res/layout/list_item_icon_text.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:orientation="vertical"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent">
+
+    <ImageView android:id="@+id/icon"
+               android:layout_width="48dp"
+               android:layout_height="48dp"
+               android:layout_gravity="center_horizontal"/>
+
+    <TextView android:id="@+id/text"
+              android:layout_gravity="center_horizontal"
+              android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:ellipsize="end"
+              android:singleLine="true"
+              android:gravity="center"
+              android:textColor="@android:color/black"
+              android:textSize="14sp"
+              />
+
+</LinearLayout>
diff --git a/android/appmgr/res/values/dimens.xml b/android/appmgr/res/values/dimens.xml
new file mode 100644
index 00000000..47c82246
--- /dev/null
+++ b/android/appmgr/res/values/dimens.xml
@@ -0,0 +1,5 @@
+<resources>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
diff --git a/android/appmgr/src/org/anbox/appmgr/AppListAdapter.java b/android/appmgr/src/org/anbox/appmgr/AppListAdapter.java
new file mode 100644
index 00000000..6146d9a0
--- /dev/null
+++ b/android/appmgr/src/org/anbox/appmgr/AppListAdapter.java
@@ -0,0 +1,89 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright 2016 Arnab Chakraborty. http://arnab.ch
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
+ * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.anbox.appmgr;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Created by Arnab Chakraborty
+ */
+public class AppListAdapter extends ArrayAdapter<AppModel> {
+    private final LayoutInflater mInflater;
+
+    public AppListAdapter (Context context) {
+        super(context, android.R.layout.simple_list_item_2);
+
+        mInflater = LayoutInflater.from(context);
+    }
+
+    public void setData(ArrayList<AppModel> data) {
+        clear();
+        if (data != null) {
+            addAll(data);
+        }
+    }
+
+    @Override
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public void addAll(Collection<? extends AppModel> items) {
+        //If the platform supports it, use addAll, otherwise add in loop
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            super.addAll(items);
+        }else{
+            for(AppModel item: items){
+                super.add(item);
+            }
+        }
+    }
+
+    /**
+     * Populate new items in the list.
+     */
+    @Override public View getView(int position, View convertView, ViewGroup parent) {
+        View view;
+
+        if (convertView == null) {
+            view = mInflater.inflate(R.layout.list_item_icon_text, parent, false);
+        } else {
+            view = convertView;
+        }
+
+        AppModel item = getItem(position);
+        ((ImageView)view.findViewById(R.id.icon)).setImageDrawable(item.getIcon());
+        ((TextView)view.findViewById(R.id.text)).setText(item.getLabel());
+
+        return view;
+    }
+}
diff --git a/android/appmgr/src/org/anbox/appmgr/AppListFragment.java b/android/appmgr/src/org/anbox/appmgr/AppListFragment.java
new file mode 100644
index 00000000..8bd2b17f
--- /dev/null
+++ b/android/appmgr/src/org/anbox/appmgr/AppListFragment.java
@@ -0,0 +1,74 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright 2016 Arnab Chakraborty. http://arnab.ch
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
+ * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.anbox.appmgr;
+
+import android.os.Bundle;
+import android.support.v4.app.ListFragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.Loader;
+import java.util.ArrayList;
+
+/**
+ * Created by Arnab Chakraborty
+ */
+public class AppListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<ArrayList<AppModel>> {
+    AppListAdapter mAdapter;
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        setEmptyText("No Applications");
+
+        mAdapter = new AppListAdapter(getActivity());
+        setListAdapter(mAdapter);
+
+        // till the data is loaded display a spinner
+        setListShown(false);
+
+        // create the loader to load the apps list in background
+        getLoaderManager().initLoader(0, null, this);
+    }
+
+    @Override
+    public Loader<ArrayList<AppModel>> onCreateLoader(int id, Bundle bundle) {
+        return new AppsLoader(getActivity());
+    }
+
+    @Override
+    public void onLoadFinished(Loader<ArrayList<AppModel>> loader, ArrayList<AppModel> apps) {
+        mAdapter.setData(apps);
+
+        if (isResumed()) {
+            setListShown(true);
+        } else {
+            setListShownNoAnimation(true);
+        }
+    }
+
+    @Override
+    public void onLoaderReset(Loader<ArrayList<AppModel>> loader) {
+        mAdapter.setData(null);
+    }
+}
diff --git a/android/appmgr/src/org/anbox/appmgr/AppModel.java b/android/appmgr/src/org/anbox/appmgr/AppModel.java
new file mode 100644
index 00000000..cc4896b1
--- /dev/null
+++ b/android/appmgr/src/org/anbox/appmgr/AppModel.java
@@ -0,0 +1,101 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright 2016 Arnab Chakraborty. http://arnab.ch
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
+ * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.anbox.appmgr;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.graphics.drawable.Drawable;
+
+import java.io.File;
+
+/**
+ * @credit http://developer.android.com/reference/android/content/AsyncTaskLoader.html
+ */
+public class AppModel {
+
+    private final Context mContext;
+    private final ApplicationInfo mInfo;
+
+    private String mAppLabel;
+    private Drawable mIcon;
+
+    private boolean mMounted;
+    private final File mApkFile;
+
+    public AppModel(Context context, ApplicationInfo info) {
+        mContext = context;
+        mInfo = info;
+
+        mApkFile = new File(info.sourceDir);
+    }
+
+    public ApplicationInfo getAppInfo() {
+        return mInfo;
+    }
+
+    public String getApplicationPackageName() {
+        return getAppInfo().packageName;
+    }
+
+    public String getLabel() {
+        return mAppLabel;
+    }
+
+    public Drawable getIcon() {
+        if (mIcon == null) {
+            if (mApkFile.exists()) {
+                mIcon = mInfo.loadIcon(mContext.getPackageManager());
+                return mIcon;
+            } else {
+                mMounted = false;
+            }
+        } else if (!mMounted) {
+            // If the app wasn't mounted but is now mounted, reload
+            // its icon.
+            if (mApkFile.exists()) {
+                mMounted = true;
+                mIcon = mInfo.loadIcon(mContext.getPackageManager());
+                return mIcon;
+            }
+        } else {
+            return mIcon;
+        }
+
+        return mContext.getResources().getDrawable(android.R.drawable.sym_def_app_icon);
+    }
+
+
+    void loadLabel(Context context) {
+        if (mAppLabel == null || !mMounted) {
+            if (!mApkFile.exists()) {
+                mMounted = false;
+                mAppLabel = mInfo.packageName;
+            } else {
+                mMounted = true;
+                CharSequence label = mInfo.loadLabel(context.getPackageManager());
+                mAppLabel = label != null ? label.toString() : mInfo.packageName;
+            }
+        }
+    }
+}
diff --git a/android/appmgr/src/org/anbox/appmgr/AppViewActivity.java b/android/appmgr/src/org/anbox/appmgr/AppViewActivity.java
new file mode 100644
index 00000000..7b92f68b
--- /dev/null
+++ b/android/appmgr/src/org/anbox/appmgr/AppViewActivity.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016 Simon Fels <morphis@gravedo.de>
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3, as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranties of
+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.anbox.appmgr;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.support.v4.app.FragmentActivity;
+
+public final class AppViewActivity extends FragmentActivity {
+    private static final String TAG = "AnboxAppView";
+
+    @Override
+    public void onCreate(Bundle info) {
+        super.onCreate(info);
+
+        setContentView(R.layout.app_view);
+
+        Log.i(TAG, "Created application view activity");
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.i(TAG, "Destroying application view activity");
+        super.onDestroy();
+    }
+}
diff --git a/android/appmgr/src/org/anbox/appmgr/AppsGridFragment.java b/android/appmgr/src/org/anbox/appmgr/AppsGridFragment.java
new file mode 100644
index 00000000..6b45672f
--- /dev/null
+++ b/android/appmgr/src/org/anbox/appmgr/AppsGridFragment.java
@@ -0,0 +1,90 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright 2016 Arnab Chakraborty. http://arnab.ch
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
+ * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.anbox.appmgr;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.Loader;
+import android.view.View;
+import android.widget.GridView;
+
+import java.util.ArrayList;
+
+/**
+ * Created by Arnab Chakraborty
+ */
+public class AppsGridFragment extends GridFragment implements LoaderManager.LoaderCallbacks<ArrayList<AppModel>> {
+
+    AppListAdapter mAdapter;
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        setEmptyText("No Applications");
+
+        mAdapter = new AppListAdapter(getActivity());
+        setGridAdapter(mAdapter);
+
+        // till the data is loaded display a spinner
+        setGridShown(false);
+
+        // create the loader to load the apps list in background
+        getLoaderManager().initLoader(0, null, this);
+    }
+
+    @Override
+    public Loader<ArrayList<AppModel>> onCreateLoader(int id, Bundle bundle) {
+        return new AppsLoader(getActivity());
+    }
+
+    @Override
+    public void onLoadFinished(Loader<ArrayList<AppModel>> loader, ArrayList<AppModel> apps) {
+        mAdapter.setData(apps);
+
+        if (isResumed()) {
+            setGridShown(true);
+        } else {
+            setGridShownNoAnimation(true);
+        }
+    }
+
+    @Override
+    public void onLoaderReset(Loader<ArrayList<AppModel>> loader) {
+        mAdapter.setData(null);
+    }
+
+    @Override
+    public void onGridItemClick(GridView g, View v, int position, long id) {
+        AppModel app = (AppModel) getGridAdapter().getItem(position);
+        if (app != null) {
+            Intent intent = getActivity().getPackageManager().getLaunchIntentForPackage(app.getApplicationPackageName());
+
+            if (intent != null) {
+                startActivity(intent);
+            }
+        }
+    }
+}
diff --git a/android/appmgr/src/org/anbox/appmgr/AppsLoader.java b/android/appmgr/src/org/anbox/appmgr/AppsLoader.java
new file mode 100644
index 00000000..5f1dee64
--- /dev/null
+++ b/android/appmgr/src/org/anbox/appmgr/AppsLoader.java
@@ -0,0 +1,184 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright 2016 Arnab Chakraborty. http://arnab.ch
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
+ * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.anbox.appmgr;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.support.v4.content.AsyncTaskLoader;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Comparator;
+
+/**
+ * @credit http://developer.android.com/reference/android/content/AsyncTaskLoader.html
+ */
+public class AppsLoader extends AsyncTaskLoader<ArrayList<AppModel>> {
+    ArrayList<AppModel> mInstalledApps;
+
+    final PackageManager mPm;
+    PackageIntentReceiver mPackageObserver;
+
+    public AppsLoader(Context context) {
+        super(context);
+
+        mPm = context.getPackageManager();
+    }
+
+    @Override
+    public ArrayList<AppModel> loadInBackground() {
+        // retrieve the list of installed applications
+        List<ApplicationInfo> apps = mPm.getInstalledApplications(0);
+
+        if (apps == null) {
+            apps = new ArrayList<ApplicationInfo>();
+        }
+
+        final Context context = getContext();
+
+        // create corresponding apps and load their labels
+        ArrayList<AppModel> items = new ArrayList<AppModel>(apps.size());
+        for (int i = 0; i < apps.size(); i++) {
+            String pkg = apps.get(i).packageName;
+
+            // only apps which are launchable
+            if (context.getPackageManager().getLaunchIntentForPackage(pkg) != null) {
+                AppModel app = new AppModel(context, apps.get(i));
+                app.loadLabel(context);
+                items.add(app);
+            }
+        }
+
+        // sort the list
+        Collections.sort(items, ALPHA_COMPARATOR);
+
+        return items;
+    }
+
+    @Override
+    public void deliverResult(ArrayList<AppModel> apps) {
+        if (isReset()) {
+            // An async query came in while the loader is stopped.  We
+            // don't need the result.
+            if (apps != null) {
+                onReleaseResources(apps);
+            }
+        }
+
+        ArrayList<AppModel> oldApps = apps;
+        mInstalledApps = apps;
+
+        if (isStarted()) {
+            // If the Loader is currently started, we can immediately
+            // deliver its results.
+            super.deliverResult(apps);
+        }
+
+        // At this point we can release the resources associated with
+        // 'oldApps' if needed; now that the new result is delivered we
+        // know that it is no longer in use.
+        if (oldApps != null) {
+            onReleaseResources(oldApps);
+        }
+    }
+
+    @Override
+    protected void onStartLoading() {
+        if (mInstalledApps != null) {
+            // If we currently have a result available, deliver it
+            // immediately.
+            deliverResult(mInstalledApps);
+        }
+
+        // watch for changes in app install and uninstall operation
+        if (mPackageObserver == null) {
+            mPackageObserver = new PackageIntentReceiver(this);
+        }
+
+        if (takeContentChanged() || mInstalledApps == null ) {
+            // If the data has changed since the last time it was loaded
+            // or is not currently available, start a load.
+            forceLoad();
+        }
+    }
+
+    @Override
+    protected void onStopLoading() {
+        // Attempt to cancel the current load task if possible.
+        cancelLoad();
+    }
+
+    @Override
+    public void onCanceled(ArrayList<AppModel> apps) {
+        super.onCanceled(apps);
+
+        // At this point we can release the resources associated with 'apps'
+        // if needed.
+        onReleaseResources(apps);
+    }
+
+    @Override
+    protected void onReset() {
+        // Ensure the loader is stopped
+        onStopLoading();
+
+        // At this point we can release the resources associated with 'apps'
+        // if needed.
+        if (mInstalledApps != null) {
+            onReleaseResources(mInstalledApps);
+            mInstalledApps = null;
+        }
+
+        // Stop monitoring for changes.
+        if (mPackageObserver != null) {
+            getContext().unregisterReceiver(mPackageObserver);
+            mPackageObserver = null;
+        }
+    }
+
+    /**
+     * Helper method to do the cleanup work if needed, for example if we're
+     * using Cursor, then we should be closing it here
+     *
+     * @param apps
+     */
+    protected void onReleaseResources(ArrayList<AppModel> apps) {
+        // do nothing
+    }
+
+
+    /**
+     * Perform alphabetical comparison of application entry objects.
+     */
+    public static final Comparator<AppModel> ALPHA_COMPARATOR = new Comparator<AppModel>() {
+        private final Collator sCollator = Collator.getInstance();
+        @Override
+        public int compare(AppModel object1, AppModel object2) {
+            return sCollator.compare(object1.getLabel(), object2.getLabel());
+        }
+    };
+}
diff --git a/android/appmgr/src/org/anbox/appmgr/GridFragment.java b/android/appmgr/src/org/anbox/appmgr/GridFragment.java
new file mode 100644
index 00000000..779aa463
--- /dev/null
+++ b/android/appmgr/src/org/anbox/appmgr/GridFragment.java
@@ -0,0 +1,399 @@
+package org.anbox.appmgr;
+
+/*
+ * Created by Thomas Barrasso on 9/11/12.
+ * Copyright (c) 2012 Loupe Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.app.Fragment;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.FrameLayout;
+import android.widget.GridView;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+/**
+ * Based on {@link android.app.ListFragment} but adapted for {@link GridView}.
+ */
+public class GridFragment extends Fragment {
+
+    static final int INTERNAL_EMPTY_ID = 0x00ff0001;
+    static final int INTERNAL_PROGRESS_CONTAINER_ID = 0x00ff0002;
+    static final int INTERNAL_LIST_CONTAINER_ID = 0x00ff0003;
+
+    final private Handler mHandler = new Handler();
+
+    final private Runnable mRequestFocus = new Runnable() {
+        public void run() {
+            mGrid.focusableViewAvailable(mGrid);
+        }
+    };
+
+    final private AdapterView.OnItemClickListener mOnClickListener
+            = new AdapterView.OnItemClickListener() {
+        public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+            onGridItemClick((GridView) parent, v, position, id);
+        }
+    };
+
+    ListAdapter mAdapter;
+    GridView mGrid;
+    View mEmptyView;
+    TextView mStandardEmptyView;
+    View mProgressContainer;
+    View mGridContainer;
+    CharSequence mEmptyText;
+    boolean mGridShown;
+
+    public GridFragment() { }
+
+    /**
+     * Provide default implementation to return a simple grid view.  Subclasses
+     * can override to replace with their own layout.  If doing so, the
+     * returned view hierarchy <em>must</em> have a GridView whose id
+     * is {@link android.R.id#list android.R.id.list} and can optionally
+     * have a sibling view id {@link android.R.id#empty android.R.id.empty}
+     * that is to be shown when the grid is empty.
+     *
+     * <p>If you are overriding this method with your own custom content,
+     * consider including the standard layout {@link android.R.layout#list_content}
+     * in your layout file, so that you continue to retain all of the standard
+     * behavior of ListFragment.  In particular, this is currently the only
+     * way to have the built-in indeterminant progress state be shown.
+     */
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        final Context context = getActivity();
+
+        FrameLayout root = new FrameLayout(context);
+
+        // ------------------------------------------------------------------
+
+        LinearLayout pframe = new LinearLayout(context);
+        pframe.setId(INTERNAL_PROGRESS_CONTAINER_ID);
+        pframe.setOrientation(LinearLayout.VERTICAL);
+        pframe.setVisibility(View.GONE);
+        pframe.setGravity(Gravity.CENTER);
+
+        ProgressBar progress = new ProgressBar(context, null,
+                android.R.attr.progressBarStyleLarge);
+        pframe.addView(progress, new FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+
+        root.addView(pframe, new FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+
+        // ------------------------------------------------------------------
+
+        FrameLayout lframe = new FrameLayout(context);
+        lframe.setId(INTERNAL_LIST_CONTAINER_ID);
+
+        TextView tv = new TextView(getActivity());
+        tv.setId(INTERNAL_EMPTY_ID);
+        tv.setGravity(Gravity.CENTER);
+        lframe.addView(tv, new FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+
+        GridView lv = new GridView(getActivity());
+        lv.setId(android.R.id.list);
+        lv.setDrawSelectorOnTop(false);
+        lv.setColumnWidth(convertDpToPixels(60, getActivity()));
+        lv.setStretchMode(GridView.STRETCH_COLUMN_WIDTH);
+        lv.setNumColumns(GridView.AUTO_FIT);
+        lv.setHorizontalSpacing(convertDpToPixels(20, getActivity()));
+        lv.setVerticalSpacing(convertDpToPixels(20, getActivity()));
+        lv.setSmoothScrollbarEnabled(true);
+
+        // disable overscroll
+        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+            lv.setOverScrollMode(ListView.OVER_SCROLL_NEVER);
+        }
+
+        lframe.addView(lv, new FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+
+        root.addView(lframe, new FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+
+        // ------------------------------------------------------------------
+
+        root.setLayoutParams(new FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+
+        return root;
+    }
+
+    /**
+     * Attach to grid view once the view hierarchy has been created.
+     */
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        ensureGrid();
+    }
+
+    /**
+     * Detach from {@link GridView}
+     */
+    @Override
+    public void onDestroyView() {
+        mHandler.removeCallbacks(mRequestFocus);
+        mGrid = null;
+        mGridShown = false;
+        mEmptyView = mProgressContainer = mGridContainer = null;
+        mStandardEmptyView = null;
+        super.onDestroyView();
+    }
+
+    public static int convertDpToPixels(float dp, Context context){
+        Resources resources = context.getResources();
+        return (int) TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP,
+                dp,
+                resources.getDisplayMetrics()
+        );
+    }
+
+    /**
+     * This method will be called when an item in the grid is selected.
+     * Subclasses should override. Subclasses can call
+     * getGridView().getItemAtPosition(position) if they need to access the
+     * data associated with the selected item.
+     *
+     * @param g The {@link GridView} where the click happened
+     * @param v The view that was clicked within the {@link GridView}
+     * @param position The position of the view in the grid
+     * @param id The row id of the item that was clicked
+     */
+    public void onGridItemClick(GridView g, View v, int position, long id) {
+
+    }
+
+    /**
+     * Provide the cursor for the {@link GridView}.
+     */
+    public void setGridAdapter(ListAdapter adapter) {
+        final boolean hadAdapter = (mAdapter != null);
+        mAdapter = adapter;
+        if (mGrid != null) {
+            mGrid.setAdapter(adapter);
+            if (!mGridShown && !hadAdapter) {
+                // The grid was hidden, and previously didn't have an
+                // adapter.  It is now time to show it.
+                setGridShown(true, (getView().getWindowToken() != null));
+            }
+        }
+    }
+
+    /**
+     * Set the currently selected grid item to the specified
+     * position with the adapter's data
+     *
+     * @param position
+     */
+    public void setSelection(int position) {
+        ensureGrid();
+        mGrid.setSelection(position);
+    }
+
+    /**
+     * Get the position of the currently selected grid item.
+     */
+    public int getSelectedItemPosition() {
+        ensureGrid();
+        return mGrid.getSelectedItemPosition();
+    }
+
+    /**
+     * Get the cursor row ID of the currently selected grid item.
+     */
+    public long getSelectedItemId() {
+        ensureGrid();
+        return mGrid.getSelectedItemId();
+    }
+
+    /**
+     * Get the activity's {@link GridView} widget.
+     */
+    public GridView getGridView() {
+        ensureGrid();
+        return mGrid;
+    }
+
+    /**
+     * The default content for a ListFragment has a TextView that can
+     * be shown when the grid is empty.  If you would like to have it
+     * shown, call this method to supply the text it should use.
+     */
+    public void setEmptyText(CharSequence text) {
+        ensureGrid();
+        if (mStandardEmptyView == null) {
+            throw new IllegalStateException("Can't be used with a custom content view");
+        }
+        mStandardEmptyView.setText(text);
+        if (mEmptyText == null) {
+            mGrid.setEmptyView(mStandardEmptyView);
+        }
+        mEmptyText = text;
+    }
+
+    /**
+     * Control whether the grid is being displayed.  You can make it not
+     * displayed if you are waiting for the initial data to show in it.  During
+     * this time an indeterminant progress indicator will be shown instead.
+     *
+     * <p>Applications do not normally need to use this themselves.  The default
+     * behavior of ListFragment is to start with the grid not being shown, only
+     * showing it once an adapter is given with {@link #setGridAdapter(ListAdapter)}.
+     * If the grid at that point had not been shown, when it does get shown
+     * it will be do without the user ever seeing the hidden state.
+     *
+     * @param shown If true, the grid view is shown; if false, the progress
+     * indicator.  The initial value is true.
+     */
+    public void setGridShown(boolean shown) {
+        setGridShown(shown, true);
+    }
+
+    /**
+     * Like {@link #setGridShown(boolean)}, but no animation is used when
+     * transitioning from the previous state.
+     */
+    public void setGridShownNoAnimation(boolean shown) {
+        setGridShown(shown, false);
+    }
+
+    /**
+     * Control whether the grid is being displayed.  You can make it not
+     * displayed if you are waiting for the initial data to show in it.  During
+     * this time an indeterminant progress indicator will be shown instead.
+     *
+     * @param shown If true, the grid view is shown; if false, the progress
+     * indicator.  The initial value is true.
+     * @param animate If true, an animation will be used to transition to the
+     * new state.
+     */
+    private void setGridShown(boolean shown, boolean animate) {
+        ensureGrid();
+        if (mProgressContainer == null) {
+            throw new IllegalStateException("Can't be used with a custom content view");
+        }
+        if (mGridShown == shown) {
+            return;
+        }
+        mGridShown = shown;
+        if (shown) {
+            if (animate) {
+                mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
+                        getActivity(), android.R.anim.fade_out));
+                mGridContainer.startAnimation(AnimationUtils.loadAnimation(
+                        getActivity(), android.R.anim.fade_in));
+            } else {
+                mProgressContainer.clearAnimation();
+                mGridContainer.clearAnimation();
+            }
+            mProgressContainer.setVisibility(View.GONE);
+            mGridContainer.setVisibility(View.VISIBLE);
+        } else {
+            if (animate) {
+                mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
+                        getActivity(), android.R.anim.fade_in));
+                mGridContainer.startAnimation(AnimationUtils.loadAnimation(
+                        getActivity(), android.R.anim.fade_out));
+            } else {
+                mProgressContainer.clearAnimation();
+                mGridContainer.clearAnimation();
+            }
+            mProgressContainer.setVisibility(View.VISIBLE);
+            mGridContainer.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * Get the ListAdapter associated with this activity's {@link GridView}.
+     */
+    public ListAdapter getGridAdapter() {
+        return mAdapter;
+    }
+
+    private void ensureGrid() {
+        if (mGrid != null) {
+            return;
+        }
+        View root = getView();
+        if (root == null) {
+            throw new IllegalStateException("Content view not yet created");
+        }
+        if (root instanceof GridView) {
+            mGrid = (GridView) root;
+        } else {
+            mStandardEmptyView = (TextView)root.findViewById(INTERNAL_EMPTY_ID);
+            if (mStandardEmptyView == null) {
+                mEmptyView = root.findViewById(android.R.id.empty);
+            } else {
+                mStandardEmptyView.setVisibility(View.GONE);
+            }
+            mProgressContainer = root.findViewById(INTERNAL_PROGRESS_CONTAINER_ID);
+            mGridContainer = root.findViewById(INTERNAL_LIST_CONTAINER_ID);
+            View rawGridView = root.findViewById(android.R.id.list);
+            if (!(rawGridView instanceof GridView)) {
+                if (rawGridView == null) {
+                    throw new RuntimeException(
+                            "Your content must have a GridView whose id attribute is " +
+                                    "'android.R.id.list'");
+                }
+                throw new RuntimeException(
+                        "Content has view with id attribute 'android.R.id.list' "
+                                + "that is not a GridView class");
+            }
+            mGrid = (GridView) rawGridView;
+            if (mEmptyView != null) {
+                mGrid.setEmptyView(mEmptyView);
+            } else if (mEmptyText != null) {
+                mStandardEmptyView.setText(mEmptyText);
+                mGrid.setEmptyView(mStandardEmptyView);
+            }
+        }
+        mGridShown = true;
+        mGrid.setOnItemClickListener(mOnClickListener);
+        if (mAdapter != null) {
+            ListAdapter adapter = mAdapter;
+            mAdapter = null;
+            setGridAdapter(adapter);
+        } else {
+            // We are starting without an adapter, so assume we won't
+            // have our data right away and start with the progress indicator.
+            if (mProgressContainer != null) {
+                setGridShown(false, false);
+            }
+        }
+        mHandler.post(mRequestFocus);
+    }
+}
diff --git a/android/appmgr/src/org/anbox/appmgr/PackageIntentReceiver.java b/android/appmgr/src/org/anbox/appmgr/PackageIntentReceiver.java
new file mode 100644
index 00000000..7d19ce7c
--- /dev/null
+++ b/android/appmgr/src/org/anbox/appmgr/PackageIntentReceiver.java
@@ -0,0 +1,62 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright 2016 Arnab Chakraborty. http://arnab.ch
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
+ * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.anbox.appmgr;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+/**
+ * Helper class to look for interesting changes to the installed apps
+ * so that the loader can be updated.
+ *
+ * @Credit http://developer.android.com/reference/android/content/AsyncTaskLoader.html
+ */
+public class PackageIntentReceiver extends BroadcastReceiver {
+
+    final AppsLoader mLoader;
+
+    public PackageIntentReceiver(AppsLoader loader) {
+        mLoader = loader;
+
+        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        filter.addDataScheme("package");
+        mLoader.getContext().registerReceiver(this, filter);
+
+        // Register for events related to sdcard installation.
+        IntentFilter sdFilter = new IntentFilter();
+        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
+        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
+        mLoader.getContext().registerReceiver(this, sdFilter);
+    }
+
+    @Override public void onReceive(Context context, Intent intent) {
+        // Tell the loader about the change.
+        mLoader.onContentChanged();
+    }
+
+}
-- 
GitLab