UI Refresh: a new app info header for app/notification.

- moved force stop and uninstall button to bottom of page (as footer)
- Forked appheader layout file, and created AppHeaderController to
  contain all binding logic for header.

Bug: 32442716
Test: RunSettingsRoboTests
Change-Id: Id4eb365ca25e035c043c068867f5cbc3a202b201
diff --git a/res/layout/app_action_buttons.xml b/res/layout/app_action_buttons.xml
new file mode 100644
index 0000000..5b00205
--- /dev/null
+++ b/res/layout/app_action_buttons.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2016 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:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="bottom"
+    android:paddingTop="4dp"
+    android:paddingStart="8dp"
+    android:paddingEnd="8dp"
+    android:orientation="horizontal">
+
+    <Button
+        android:id="@+id/left_button"
+        style="@style/AppActionPrimaryButton"
+        android:layout_width="0dp"
+        android:layout_weight="1"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:paddingEnd="8dp"/>
+
+    <Button
+        android:id="@+id/right_button"
+        style="@style/AppActionPrimaryButton"
+        android:layout_width="0dp"
+        android:layout_weight="1"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:paddingStart="8dp"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/app_details.xml b/res/layout/app_details.xml
new file mode 100644
index 0000000..68cc57d
--- /dev/null
+++ b/res/layout/app_details.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2016 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:id="@+id/app_snippet"
+    style="@style/AppHeader"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center_horizontal|top"
+    android:orientation="vertical"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+    <!-- App snippet with buttons -->
+    <ImageView
+        android:id="@android:id/icon"
+        android:layout_marginTop="16dp"
+        android:layout_width="80dp"
+        android:layout_height="80dp"
+        android:scaleType="fitXY"
+        android:layout_gravity="center_horizontal"
+        android:antialias="true"/>
+
+    <TextView
+        android:id="@android:id/title"
+        style="@style/TextAppearance.AppHeaderTitle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:singleLine="true"
+        android:ellipsize="marquee"
+        android:gravity="center_horizontal"
+        android:paddingTop="8dp"/>
+
+    <TextView
+        android:id="@android:id/summary"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:singleLine="true"
+        android:ellipsize="marquee"
+        android:textAppearance="@android:style/TextAppearance.Material.Body1"
+        android:textColor="?android:attr/textColorSecondary"/>
+
+    <LinearLayout
+        android:id="@+id/app_detail_links"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="bottom"
+        android:paddingStart="8dp"
+        android:paddingEnd="8dp"
+        android:orientation="horizontal">
+
+        <ImageButton
+            android:id="@+id/left_button"
+            style="@style/AppHeaderLinkButton"
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:paddingEnd="8dp"
+            android:tint="?android:attr/colorAccent"/>
+
+        <ImageButton
+            android:id="@+id/right_button"
+            style="@style/AppHeaderLinkButton"
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:paddingStart="8dp"
+            android:src="@drawable/ic_settings_24dp"
+            android:tint="?android:attr/colorAccent"/>
+
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 0f957a8..9dab013 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -452,4 +452,22 @@
         <item name="android:progressDrawable">@drawable/ring_progress</item>
     </style>
 
+    <style name="AppHeader">
+        <item name="android:background">@color/card_background_grey</item>
+        <item name="android:gravity">center_horizontal</item>
+        <item name="android:paddingStart">16dp</item>
+        <item name="android:paddingEnd">16dp</item>
+        <item name="android:paddingBottom">16dp</item>
+    </style>
+
+    <style name="TextAppearance.AppHeaderTitle"
+           parent="@android:style/TextAppearance.Material.Subhead">
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textSize">24sp</item>
+    </style>
+
+    <style name="AppHeaderLinkButton" parent="android:Widget.Material.Button.Borderless"/>
+
+    <style name="AppActionPrimaryButton" parent="android:Widget.Material.Button.Colored"/>
+
 </resources>
diff --git a/res/xml/installed_app_details_ia.xml b/res/xml/installed_app_details_ia.xml
new file mode 100644
index 0000000..f4603ed
--- /dev/null
+++ b/res/xml/installed_app_details_ia.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2016 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.
+  -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <com.android.settings.applications.LayoutPreference
+        android:key="header_view"
+        android:layout="@layout/app_details"
+        android:selectable="false"/>
+
+    <Preference
+        android:key="storage_settings"
+        android:title="@string/storage_settings"
+        android:selectable="true"/>
+
+    <Preference
+        android:key="data_settings"
+        android:title="@string/data_usage_summary_title"
+        android:selectable="true"/>
+
+    <Preference
+        android:key="permission_settings"
+        android:title="@string/permissions_label"
+        android:selectable="true"/>
+
+    <Preference
+        android:key="notification_settings"
+        android:title="@string/notifications_label"
+        android:selectable="true"/>
+
+    <Preference
+        android:key="preferred_settings"
+        android:title="@string/launch_by_default"
+        android:selectable="true"/>
+
+    <Preference
+        android:key="battery"
+        android:title="@string/power_usage_summary_title"
+        android:selectable="true"/>
+
+    <Preference
+        android:key="memory"
+        android:title="@string/memory_settings_title"
+        android:enabled="false"
+        android:selectable="true"/>
+
+</PreferenceScreen>
\ No newline at end of file
diff --git a/src/com/android/settings/applications/AppHeaderController.java b/src/com/android/settings/applications/AppHeaderController.java
new file mode 100644
index 0000000..f4d87a0
--- /dev/null
+++ b/src/com/android/settings/applications/AppHeaderController.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.settings.applications;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.support.annotation.IntDef;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.settings.R;
+import com.android.settings.Utils;
+import com.android.settingslib.applications.ApplicationsState;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+public class AppHeaderController {
+
+    @IntDef({ActionType.ACTION_APP_INFO,
+            ActionType.ACTION_APP_PREFERENCE,
+            ActionType.ACTION_STORE_DEEP_LINK,
+            ActionType.ACTION_NOTIF_PREFERENCE})
+    @Retention(RetentionPolicy.SOURCE)
+    @interface ActionType {
+        int ACTION_APP_INFO = 0;
+        int ACTION_STORE_DEEP_LINK = 1;
+        int ACTION_APP_PREFERENCE = 2;
+        int ACTION_NOTIF_PREFERENCE = 3;
+    }
+
+    private static final String TAG = "AppDetailFeature";
+
+    private final Context mContext;
+
+    public AppHeaderController(Context context) {
+        mContext = context;
+    }
+
+    public void bindAppHeader(View appSnippet, PackageInfo packageInfo,
+            ApplicationsState.AppEntry appEntry) {
+        final String versionName = packageInfo == null ? null : packageInfo.versionName;
+        final Resources res = appSnippet.getResources();
+
+        // Set Icon
+        final ImageView iconView = (ImageView) appSnippet.findViewById(android.R.id.icon);
+        if (appEntry.icon != null) {
+            iconView.setImageDrawable(appEntry.icon.getConstantState().newDrawable(res));
+        }
+
+        // Set application name.
+        final TextView labelView = (TextView) appSnippet.findViewById(android.R.id.title);
+        labelView.setText(appEntry.label);
+
+        // Version number of application
+        final TextView appVersion = (TextView) appSnippet.findViewById(android.R.id.summary);
+
+        if (!TextUtils.isEmpty(versionName)) {
+            appVersion.setSelected(true);
+            appVersion.setVisibility(View.VISIBLE);
+            appVersion.setText(res.getString(R.string.version_text, String.valueOf(versionName)));
+        } else {
+            appVersion.setVisibility(View.INVISIBLE);
+        }
+    }
+
+    public void bindAppHeaderButtons(Fragment fragment, View appLinkButtons, String packageName,
+            @ActionType int leftAction, @ActionType int rightAction) {
+        ImageButton leftButton = (ImageButton) appLinkButtons.findViewById(R.id.left_button);
+        ImageButton rightButton = (ImageButton) appLinkButtons.findViewById(R.id.right_button);
+
+        bindAppDetailButton(fragment, packageName, leftButton, leftAction);
+        bindAppDetailButton(fragment, packageName, rightButton, rightAction);
+    }
+
+    private void bindAppDetailButton(Fragment fragment, String packageName,
+            ImageButton button, @ActionType int action) {
+        if (button == null) {
+            return;
+        }
+        switch (action) {
+            case ActionType.ACTION_APP_INFO: {
+                if (packageName == null || packageName.equals(Utils.OS_PKG)) {
+                    button.setVisibility(View.GONE);
+                } else {
+                    // TODO
+                    button.setImageResource(com.android.settings.R.drawable.ic_info);
+                    button.setVisibility(View.VISIBLE);
+                }
+                return;
+            }
+            case ActionType.ACTION_STORE_DEEP_LINK: {
+                final Intent intent = new Intent(Intent.ACTION_SHOW_APP_INFO)
+                        .setPackage(getInstallerPackageName(mContext, packageName));
+                final Intent result = resolveIntent(intent);
+                if (result == null) {
+                    button.setVisibility(View.GONE);
+                } else {
+                    result.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
+                    button.setImageResource(R.drawable.ic_sim_sd);
+                    button.setOnClickListener(v -> fragment.startActivity(intent));
+                    button.setVisibility(View.VISIBLE);
+                }
+                return;
+            }
+            case ActionType.ACTION_NOTIF_PREFERENCE: {
+                // TODO
+                return;
+            }
+            case ActionType.ACTION_APP_PREFERENCE: {
+                final Intent intent = resolveIntent(
+                        new Intent(Intent.ACTION_APPLICATION_PREFERENCES).setPackage(packageName));
+                if (intent == null) {
+                    button.setVisibility(View.GONE);
+                    return;
+                }
+                button.setOnClickListener(v -> fragment.startActivity(intent));
+                button.setVisibility(View.VISIBLE);
+                return;
+            }
+        }
+    }
+
+    private String getInstallerPackageName(Context context, String packageName) {
+        try {
+            return context.getPackageManager().getInstallerPackageName(packageName);
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "Exception while retrieving the package installer of " + packageName, e);
+            return null;
+        }
+    }
+
+    private Intent resolveIntent(Intent i) {
+        ResolveInfo result = mContext.getPackageManager().resolveActivity(i, 0);
+        if (result != null) {
+            return new Intent(i.getAction())
+                    .setClassName(result.activityInfo.packageName, result.activityInfo.name);
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/settings/applications/ApplicationFeatureProvider.java b/src/com/android/settings/applications/ApplicationFeatureProvider.java
new file mode 100644
index 0000000..710f02f
--- /dev/null
+++ b/src/com/android/settings/applications/ApplicationFeatureProvider.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.settings.applications;
+
+public interface ApplicationFeatureProvider {
+
+    AppHeaderController getAppHeaderController();
+}
+
diff --git a/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java b/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java
new file mode 100644
index 0000000..3223a91
--- /dev/null
+++ b/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.settings.applications;
+
+import android.content.Context;
+
+public class ApplicationFeatureProviderImpl implements ApplicationFeatureProvider {
+
+    private final Context mContext;
+
+    private AppHeaderController mAppHeaderController;
+
+    public ApplicationFeatureProviderImpl(Context context) {
+        mContext = context.getApplicationContext();
+    }
+
+    @Override
+    public AppHeaderController getAppHeaderController() {
+        if (mAppHeaderController == null) {
+            mAppHeaderController = new AppHeaderController(mContext);
+        }
+        return mAppHeaderController;
+    }
+}
diff --git a/src/com/android/settings/applications/InstalledAppDetails.java b/src/com/android/settings/applications/InstalledAppDetails.java
index 38d4f49..6ffc4c7 100755
--- a/src/com/android/settings/applications/InstalledAppDetails.java
+++ b/src/com/android/settings/applications/InstalledAppDetails.java
@@ -84,6 +84,7 @@
 import com.android.settings.SettingsPreferenceFragment;
 import com.android.settings.Utils;
 import com.android.settings.applications.PermissionsSummaryHelper.PermissionsResultCallback;
+import com.android.settings.dashboard.DashboardFeatureProvider;
 import com.android.settings.datausage.AppDataUsage;
 import com.android.settings.datausage.DataUsageList;
 import com.android.settings.datausage.DataUsageSummary;
@@ -92,6 +93,7 @@
 import com.android.settings.notification.AppNotificationSettings;
 import com.android.settings.notification.NotificationBackend;
 import com.android.settings.notification.NotificationBackend.AppRow;
+import com.android.settings.overlay.FeatureFactory;
 import com.android.settingslib.AppItem;
 import com.android.settingslib.RestrictedLockUtils;
 import com.android.settingslib.applications.AppUtils;
@@ -138,6 +140,7 @@
     private static final int DLG_SPECIAL_DISABLE = DLG_BASE + 3;
 
     private static final String KEY_HEADER = "header_view";
+    private static final String KEY_FOOTER = "header_footer";
     private static final String KEY_NOTIFICATION = "notification_settings";
     private static final String KEY_STORAGE = "storage_settings";
     private static final String KEY_PERMISSION = "permission_settings";
@@ -148,11 +151,15 @@
 
     private static final String NOTIFICATION_TUNER_SETTING = "show_importance_slider";
 
-    private final HashSet<String> mHomePackages = new HashSet<String>();
+    private final HashSet<String> mHomePackages = new HashSet<>();
+
+    private DashboardFeatureProvider mDashboardFeatureProvider;
+    private AppHeaderController mAppHeaderController;
 
     private boolean mInitialized;
     private boolean mShowUninstalled;
     private LayoutPreference mHeader;
+    private LayoutPreference mFooter;
     private Button mUninstallButton;
     private boolean mUpdatedSysApp = false;
     private Button mForceStopButton;
@@ -164,6 +171,7 @@
     private Preference mMemoryPreference;
 
     private boolean mDisableAfterUninstall;
+
     // Used for updating notification preference.
     private final NotificationBackend mBackend = new NotificationBackend();
 
@@ -306,9 +314,16 @@
     @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
+        final Activity activity = getActivity();
+        mDashboardFeatureProvider =
+                FeatureFactory.getFactory(activity).getDashboardFeatureProvider(activity);
+        mAppHeaderController = FeatureFactory.getFactory(activity)
+                .getApplicationFeatureProvider(activity).getAppHeaderController();
 
         setHasOptionsMenu(true);
-        addPreferencesFromResource(R.xml.installed_app_details);
+        addPreferencesFromResource(mDashboardFeatureProvider.isEnabled()
+                ? R.xml.installed_app_details_ia
+                : R.xml.installed_app_details);
         addDynamicPrefs();
 
         if (Utils.isBandwidthControlEnabled()) {
@@ -366,7 +381,18 @@
         if (mFinishing) {
             return;
         }
-        handleHeader();
+        if (!mDashboardFeatureProvider.isEnabled()) {
+            handleHeader();
+        } else {
+            mHeader = (LayoutPreference) findPreference(KEY_HEADER);
+            mAppHeaderController.bindAppHeaderButtons(
+                    this,
+                    mHeader.findViewById(R.id.app_detail_links),
+                    mPackageName,
+                    AppHeaderController.ActionType.ACTION_STORE_DEEP_LINK,
+                    AppHeaderController.ActionType.ACTION_APP_PREFERENCE);
+            prepareUninstallAndStop();
+        }
 
         mNotificationPreference = findPreference(KEY_NOTIFICATION);
         mNotificationPreference.setOnPreferenceClickListener(this);
@@ -404,7 +430,6 @@
 
     private void handleHeader() {
         mHeader = (LayoutPreference) findPreference(KEY_HEADER);
-
         // Get Control button panel
         View btnPanel = mHeader.findViewById(R.id.control_buttons_panel);
         mForceStopButton = (Button) btnPanel.findViewById(R.id.right_button);
@@ -429,6 +454,13 @@
         }
     }
 
+    private void prepareUninstallAndStop() {
+        mForceStopButton = (Button) mFooter.findViewById(R.id.right_button);
+        mForceStopButton.setText(R.string.force_stop);
+        mUninstallButton = (Button) mFooter.findViewById(R.id.left_button);
+        mForceStopButton.setEnabled(false);
+    }
+
     private Intent resolveIntent(Intent i) {
         ResolveInfo result = getContext().getPackageManager().resolveActivity(i, 0);
         return result != null ? new Intent(i.getAction())
@@ -511,8 +543,12 @@
     private void setAppLabelAndIcon(PackageInfo pkgInfo) {
         final View appSnippet = mHeader.findViewById(R.id.app_snippet);
         mState.ensureIcon(mAppEntry);
-        setupAppSnippet(appSnippet, mAppEntry.label, mAppEntry.icon,
-                pkgInfo != null ? pkgInfo.versionName : null);
+        if (mDashboardFeatureProvider.isEnabled()) {
+            mAppHeaderController.bindAppHeader(appSnippet, pkgInfo, mAppEntry);
+        } else {
+            setupAppSnippet(appSnippet, mAppEntry.label, mAppEntry.icon,
+                    pkgInfo != null ? pkgInfo.versionName : null);
+        }
     }
 
     private boolean signaturesMatch(String pkg1, String pkg2) {
@@ -917,6 +953,12 @@
         }
 
         addAppInstallerInfoPref(screen);
+        if (mDashboardFeatureProvider.isEnabled()) {
+            mFooter = new LayoutPreference(screen.getContext(), R.layout.app_action_buttons);
+            mFooter.setOrder(10000);
+            mFooter.setKey(KEY_FOOTER);
+            screen.addPreference(mFooter);
+        }
     }
 
     private void addAppInstallerInfoPref(PreferenceScreen screen) {
diff --git a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java
index ed0520a..d0c7c45 100644
--- a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java
+++ b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java
@@ -38,8 +38,8 @@
     private final CategoryManager mCategoryManager;
 
     public DashboardFeatureProviderImpl(Context context) {
-        mContext = context;
-        mCategoryManager = CategoryManager.get(context);
+        mContext = context.getApplicationContext();
+        mCategoryManager = CategoryManager.get(mContext);
     }
 
     @Override
diff --git a/src/com/android/settings/overlay/FeatureFactory.java b/src/com/android/settings/overlay/FeatureFactory.java
index 35ec40f..515975b 100644
--- a/src/com/android/settings/overlay/FeatureFactory.java
+++ b/src/com/android/settings/overlay/FeatureFactory.java
@@ -21,6 +21,7 @@
 import android.util.Log;
 
 import com.android.settings.R;
+import com.android.settings.applications.ApplicationFeatureProvider;
 import com.android.settings.core.instrumentation.MetricsFeatureProvider;
 import com.android.settings.dashboard.DashboardFeatureProvider;
 import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
@@ -71,6 +72,8 @@
 
     public abstract DashboardFeatureProvider getDashboardFeatureProvider(Context context);
 
+    public abstract ApplicationFeatureProvider getApplicationFeatureProvider(Context context);
+
     public abstract LocaleFeatureProvider getLocaleFeatureProvider();
 
 
diff --git a/src/com/android/settings/overlay/FeatureFactoryImpl.java b/src/com/android/settings/overlay/FeatureFactoryImpl.java
index 976ee3c..934021d 100644
--- a/src/com/android/settings/overlay/FeatureFactoryImpl.java
+++ b/src/com/android/settings/overlay/FeatureFactoryImpl.java
@@ -19,6 +19,8 @@
 import android.content.Context;
 import android.support.annotation.Keep;
 
+import com.android.settings.applications.ApplicationFeatureProvider;
+import com.android.settings.applications.ApplicationFeatureProviderImpl;
 import com.android.settings.core.instrumentation.MetricsFeatureProvider;
 import com.android.settings.core.instrumentation.MetricsFeatureProviderImpl;
 import com.android.settings.dashboard.DashboardFeatureProvider;
@@ -33,6 +35,7 @@
 @Keep
 public final class FeatureFactoryImpl extends FeatureFactory {
 
+    private ApplicationFeatureProvider mApplicationFeatureProvider;
     private MetricsFeatureProvider mMetricsFeatureProvider;
     private DashboardFeatureProviderImpl mDashboardFeatureProvider;
     private LocaleFeatureProvider mLocaleFeatureProvider;
@@ -64,6 +67,14 @@
     }
 
     @Override
+    public ApplicationFeatureProvider getApplicationFeatureProvider(Context context) {
+        if (mApplicationFeatureProvider == null) {
+            mApplicationFeatureProvider = new ApplicationFeatureProviderImpl(context);
+        }
+        return mApplicationFeatureProvider;
+    }
+
+    @Override
     public LocaleFeatureProvider getLocaleFeatureProvider() {
         if (mLocaleFeatureProvider == null) {
             mLocaleFeatureProvider = new LocaleFeatureProviderImpl();
diff --git a/tests/robotests/src/com/android/settings/applications/AdvancedAppSettingsTest.java b/tests/robotests/src/com/android/settings/applications/AdvancedAppSettingsTest.java
index d0c5889..7da30ed 100644
--- a/tests/robotests/src/com/android/settings/applications/AdvancedAppSettingsTest.java
+++ b/tests/robotests/src/com/android/settings/applications/AdvancedAppSettingsTest.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 
 import com.android.settings.R;
+import com.android.settings.SettingsRobolectricTestRunner;
 import com.android.settings.TestConfig;
 import com.android.settings.testutils.FakeFeatureFactory;
 
@@ -28,14 +29,13 @@
 import org.mockito.Answers;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.Config;
 import org.robolectric.shadows.ShadowApplication;
 
 import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.Mockito.when;
 
-@RunWith(RobolectricTestRunner.class)
+@RunWith(SettingsRobolectricTestRunner.class)
 @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
 public class AdvancedAppSettingsTest {
 
diff --git a/tests/robotests/src/com/android/settings/applications/AppHeaderControllerTest.java b/tests/robotests/src/com/android/settings/applications/AppHeaderControllerTest.java
new file mode 100644
index 0000000..6732028
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/applications/AppHeaderControllerTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.settings.applications;
+
+
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.ResolveInfo;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.settings.R;
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+import com.android.settingslib.applications.ApplicationsState;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplication;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class AppHeaderControllerTest {
+
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private Context mContext;
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private ApplicationsState.AppEntry mAppEntry;
+    @Mock
+    private Fragment mFragment;
+
+    private Context mShadowContext;
+    private LayoutInflater mLayoutInflater;
+    private PackageInfo mInfo;
+    private AppHeaderController mController;
+
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mShadowContext = ShadowApplication.getInstance().getApplicationContext();
+        mLayoutInflater = LayoutInflater.from(mShadowContext);
+        mInfo = new PackageInfo();
+        mInfo.versionName = "1234";
+        mController = new AppHeaderController(mContext);
+    }
+
+    @Test
+    public void bindViews_shouldBindAllData() {
+        final String testString = "test";
+        final View appHeader = mLayoutInflater.inflate(R.layout.app_details, null /* root */);
+        final TextView label = (TextView) appHeader.findViewById(android.R.id.title);
+        final TextView version = (TextView) appHeader.findViewById(android.R.id.summary);
+        label.setText(testString);
+        label.setText(testString);
+
+        mController.bindAppHeader(appHeader, mInfo, mAppEntry);
+
+        assertThat(label.getText()).isNotEqualTo(testString);
+        assertThat(version.getText())
+                .isEqualTo(mShadowContext.getString(R.string.version_text, mInfo.versionName));
+    }
+
+    @Test
+    public void bindButton_hasAppPref_shouldShowButton() {
+        final ResolveInfo info = new ResolveInfo();
+        info.activityInfo = new ActivityInfo();
+        info.activityInfo.packageName = "123";
+        info.activityInfo.name = "321";
+        final View appLinks = mLayoutInflater
+                .inflate(R.layout.app_details, null /* root */);
+        when(mContext.getPackageManager().resolveActivity(any(Intent.class), anyInt()))
+                .thenReturn(info);
+        mController.bindAppHeaderButtons(mFragment, appLinks,
+                mShadowContext.getPackageName(),
+                AppHeaderController.ActionType.ACTION_APP_PREFERENCE,
+                AppHeaderController.ActionType.ACTION_APP_PREFERENCE);
+        assertThat(appLinks.findViewById(R.id.left_button).getVisibility())
+                .isEqualTo(View.VISIBLE);
+        assertThat(appLinks.findViewById(R.id.right_button).getVisibility())
+                .isEqualTo(View.VISIBLE);
+        try {
+            appLinks.findViewById(R.id.left_button).performClick();
+        } catch (Exception e) {
+            // Ignore exception because the launching intent is fake.
+        }
+        verify(mFragment).startActivity(any(Intent.class));
+    }
+
+    @Test
+    public void bindButton_noAppPref_shouldNotShowButton() {
+        final View appLinks = mLayoutInflater
+                .inflate(R.layout.app_details, null /* root */);
+        when(mContext.getPackageManager().resolveActivity(any(Intent.class), anyInt()))
+                .thenReturn(null);
+        mController.bindAppHeaderButtons(mFragment, appLinks,
+                mShadowContext.getPackageName(),
+                AppHeaderController.ActionType.ACTION_APP_PREFERENCE,
+                AppHeaderController.ActionType.ACTION_APP_PREFERENCE);
+        assertThat(appLinks.findViewById(R.id.left_button).getVisibility())
+                .isEqualTo(View.GONE);
+        assertThat(appLinks.findViewById(R.id.right_button).getVisibility())
+                .isEqualTo(View.GONE);
+    }
+
+    @Test
+    public void bindButton_noStoreLink_shouldNotShowButton() {
+        final View appLinks = mLayoutInflater
+                .inflate(R.layout.app_details, null /* root */);
+        when(mContext.getPackageManager().resolveActivity(any(Intent.class), anyInt()))
+                .thenReturn(null);
+        mController.bindAppHeaderButtons(mFragment, appLinks,
+                mShadowContext.getPackageName(),
+                AppHeaderController.ActionType.ACTION_STORE_DEEP_LINK,
+                AppHeaderController.ActionType.ACTION_STORE_DEEP_LINK);
+        assertThat(appLinks.findViewById(R.id.left_button).getVisibility())
+                .isEqualTo(View.GONE);
+        assertThat(appLinks.findViewById(R.id.right_button).getVisibility())
+                .isEqualTo(View.GONE);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java
index 7bf908d..8241352 100644
--- a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java
+++ b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java
@@ -16,6 +16,8 @@
 package com.android.settings.testutils;
 
 import android.content.Context;
+
+import com.android.settings.applications.ApplicationFeatureProvider;
 import com.android.settings.core.instrumentation.MetricsFeatureProvider;
 import com.android.settings.dashboard.DashboardFeatureProvider;
 import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
@@ -38,6 +40,7 @@
     public final PowerUsageFeatureProvider powerUsageFeatureProvider;
     public final DashboardFeatureProvider dashboardFeatureProvider;
     public final LocaleFeatureProvider localeFeatureProvider;
+    public final ApplicationFeatureProvider applicationFeatureProvider;
 
     /**
      * Call this in {@code @Before} method of the test class to use fake factory.
@@ -65,6 +68,7 @@
         powerUsageFeatureProvider = mock(PowerUsageFeatureProvider.class);
         dashboardFeatureProvider = mock(DashboardFeatureProvider.class);
         localeFeatureProvider = mock(LocaleFeatureProvider.class);
+        applicationFeatureProvider = mock(ApplicationFeatureProvider.class);
     }
 
     @Override
@@ -88,6 +92,11 @@
     }
 
     @Override
+    public ApplicationFeatureProvider getApplicationFeatureProvider(Context context) {
+        return applicationFeatureProvider;
+    }
+
+    @Override
     public LocaleFeatureProvider getLocaleFeatureProvider() {
         return localeFeatureProvider;
     }