Implement base version of GrantPermissions dialog

With the Emerald release, we need to support the new
permission APIs.  This changelist adds a dialog
for the Activity.requestPermissions() API so that
users can request permissions.

This check in is a functional version, but not polished.
will need to be a follow up CL to add the correct
animations and update the UI to the appropriate redlines.

The implementation for the confirmation dialog is modeled
after the one in the clockwork libs/Views folder.  There
are some tweaks to match the designed behavior of the
permission dialog.  When there's more time in the future,
we should try to condense this to one implementation.

BUG: 23118402

Change-Id: Ic90d37a2ce8a7adacb7c4e004b0a5260b624f5c8
diff --git a/Android.mk b/Android.mk
index 5a53086..9a7fcc4 100644
--- a/Android.mk
+++ b/Android.mk
@@ -9,12 +9,11 @@
 
 LOCAL_STATIC_JAVA_LIBRARIES += \
     android-support-v4 \
-    android-support-v7-recyclerview \
     android-support-v7-preference \
     android-support-v7-appcompat \
     android-support-v14-preference \
     android-support-v17-preference-leanback \
-    android-support-v17-leanback
+    android-support-v17-leanback \
 
 LOCAL_RESOURCE_DIR := \
     frameworks/support/v17/leanback/res \
@@ -22,11 +21,10 @@
     frameworks/support/v14/preference/res \
     frameworks/support/v17/preference-leanback/res \
     frameworks/support/v7/appcompat/res \
-    frameworks/support/v7/recyclerview/res \
     $(LOCAL_PATH)/res
 
 LOCAL_AAPT_FLAGS := --auto-add-overlay \
-    --extra-packages android.support.v17.leanback:android.support.v7.preference:android.support.v14.preference:android.support.v17.preference:android.support.v7.appcompat:android.support.v7.recyclerview
+    --extra-packages android.support.v17.leanback:android.support.v7.preference:android.support.v14.preference:android.support.v17.preference:android.support.v7.appcompat
 
 LOCAL_PACKAGE_NAME := PackageInstaller
 LOCAL_CERTIFICATE := platform
@@ -35,4 +33,6 @@
 
 LOCAL_PROGUARD_FLAG_FILES := proguard.flags
 
+include vendor/unbundled_google/libs/wearable/wearable-support.mk
+
 include $(BUILD_PACKAGE)
diff --git a/res/drawable-watch-280dpi/ic_cc_cancel.png b/res/drawable-watch-280dpi/ic_cc_cancel.png
new file mode 100644
index 0000000..249b869
--- /dev/null
+++ b/res/drawable-watch-280dpi/ic_cc_cancel.png
Binary files differ
diff --git a/res/drawable-watch-280dpi/ic_cc_checkmark.png b/res/drawable-watch-280dpi/ic_cc_checkmark.png
new file mode 100644
index 0000000..94db9ab
--- /dev/null
+++ b/res/drawable-watch-280dpi/ic_cc_checkmark.png
Binary files differ
diff --git a/res/drawable-watch-hdpi/ic_cc_cancel.png b/res/drawable-watch-hdpi/ic_cc_cancel.png
new file mode 100644
index 0000000..a57893e
--- /dev/null
+++ b/res/drawable-watch-hdpi/ic_cc_cancel.png
Binary files differ
diff --git a/res/drawable-watch-hdpi/ic_cc_checkmark.png b/res/drawable-watch-hdpi/ic_cc_checkmark.png
new file mode 100644
index 0000000..29f9ecd
--- /dev/null
+++ b/res/drawable-watch-hdpi/ic_cc_checkmark.png
Binary files differ
diff --git a/res/drawable-watch-mdpi/ic_cc_cancel.png b/res/drawable-watch-mdpi/ic_cc_cancel.png
new file mode 100644
index 0000000..87fc65a
--- /dev/null
+++ b/res/drawable-watch-mdpi/ic_cc_cancel.png
Binary files differ
diff --git a/res/drawable-watch-mdpi/ic_cc_checkmark.png b/res/drawable-watch-mdpi/ic_cc_checkmark.png
new file mode 100644
index 0000000..0989daa
--- /dev/null
+++ b/res/drawable-watch-mdpi/ic_cc_checkmark.png
Binary files differ
diff --git a/res/drawable-watch-xhdpi/ic_cc_cancel.png b/res/drawable-watch-xhdpi/ic_cc_cancel.png
new file mode 100644
index 0000000..fec6ecb
--- /dev/null
+++ b/res/drawable-watch-xhdpi/ic_cc_cancel.png
Binary files differ
diff --git a/res/drawable-watch-xhdpi/ic_cc_checkmark.png b/res/drawable-watch-xhdpi/ic_cc_checkmark.png
new file mode 100644
index 0000000..f98cc1e
--- /dev/null
+++ b/res/drawable-watch-xhdpi/ic_cc_checkmark.png
Binary files differ
diff --git a/res/drawable-watch/action_negative_bg.xml b/res/drawable-watch/action_negative_bg.xml
new file mode 100644
index 0000000..f1c33b5
--- /dev/null
+++ b/res/drawable-watch/action_negative_bg.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false">
+        <shape android:shape="oval">
+            <solid android:color="@color/circular_button_disabled"/>
+            <size android:width="40dp" android:height="40dp" />
+        </shape>
+    </item>
+    <item android:state_pressed="true">
+        <shape android:shape="oval">
+            <solid android:color="#757575"/>
+            <size android:width="40dp" android:height="40dp" />
+        </shape>
+    </item>
+    <item>
+        <shape android:shape="oval">
+            <solid android:color="#BDBDBD"/>
+            <size android:width="40dp" android:height="40dp" />
+        </shape>
+    </item>
+</selector>
diff --git a/res/drawable-watch/action_positive_bg.xml b/res/drawable-watch/action_positive_bg.xml
new file mode 100644
index 0000000..bc3e88b
--- /dev/null
+++ b/res/drawable-watch/action_positive_bg.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false">
+        <shape android:shape="oval">
+            <solid android:color="@color/circular_button_disabled"/>
+            <size android:width="40dp" android:height="40dp" />
+        </shape>
+    </item>
+    <item android:state_pressed="true">
+        <shape android:shape="oval">
+            <solid android:color="#009688"/>
+            <size android:width="40dp" android:height="40dp" />
+        </shape>
+    </item>
+    <item>
+        <shape android:shape="oval">
+            <solid android:color="#00BFA5"/>
+            <size android:width="40dp" android:height="40dp" />
+        </shape>
+    </item>
+</selector>
diff --git a/res/drawable-watch/cancel_button.xml b/res/drawable-watch/cancel_button.xml
new file mode 100644
index 0000000..5b16f54
--- /dev/null
+++ b/res/drawable-watch/cancel_button.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+    <item android:drawable="@drawable/action_negative_bg" />
+    <item>
+        <bitmap android:src="@drawable/ic_cc_cancel" android:gravity="center" />
+    </item>
+</layer-list>
diff --git a/res/drawable-watch/confirm_button.xml b/res/drawable-watch/confirm_button.xml
new file mode 100644
index 0000000..6a895ec
--- /dev/null
+++ b/res/drawable-watch/confirm_button.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+    <item android:drawable="@drawable/action_positive_bg" />
+    <item>
+        <bitmap android:src="@drawable/ic_cc_checkmark" android:gravity="center" />
+    </item>
+</layer-list>
diff --git a/res/layout-watch/grant_permissions.xml b/res/layout-watch/grant_permissions.xml
new file mode 100644
index 0000000..80ed5b9
--- /dev/null
+++ b/res/layout-watch/grant_permissions.xml
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<FrameLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:id="@+id/confirmation"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        >
+
+    <ScrollView
+            android:id="@+id/scrolling_container"
+            android:overScrollMode="never"
+            android:scrollbars="none"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+        <LinearLayout
+                android:id="@+id/content"
+                android:orientation="vertical"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+
+            <TextView
+                    android:id="@+id/current_page_text"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center"
+                    android:layout_marginBottom="@dimen/grant_permissions_app_breadcrumb_margin_bottom"
+                    android:textAppearance="@style/GrantPermissions.BreadcrumbText" />
+
+            <ImageView
+                    android:id="@+id/icon"
+                    android:layout_width="@dimen/grant_permissions_app_icon_size"
+                    android:layout_height="@dimen/grant_permissions_app_icon_size"
+                    android:tint="@color/grant_permissions_app_color"
+                    android:layout_gravity="center"
+                    android:layout_marginTop="@dimen/grant_permissions_app_icon_margin_top"/>
+
+            <TextView
+                    android:id="@+id/message"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="10dp"
+                    android:paddingLeft="16dp"
+                    android:paddingRight="16dp"
+                    android:textSize="20sp"
+                    android:gravity="center"
+                    android:fontFamily="sans-serif-condensed-light"
+                    android:textAppearance="@style/GrantPermissions.TitleText"/>
+
+            <!-- TODO: Change this to use a ViewStub instead of show/hiding the two layouts -->
+            <FrameLayout android:layout_width="match_parent"
+                         android:layout_height="wrap_content"
+                         android:id="@+id/button_bar_container"
+                         android:layout_gravity="bottom"
+                         android:background="#FF606060">
+                <android.support.wearable.view.WearableFrameLayout
+                        android:id="@+id/horizontal_button_bar"
+                        android:layout_width="match_parent"
+                        android:layout_height="72dp"
+                        app:layout_heightRound="96dp">
+                    <Button
+                            android:id="@+id/horizontal_deny_button"
+                            android:layout_width="54dp"
+                            android:layout_height="54dp"
+                            android:layout_gravity="top|left"
+                            android:layout_marginLeft="16dp"
+                            android:layout_marginTop="9dp"
+                            app:layout_marginLeftRound="36dp"
+                            app:layout_marginTopRound="12dp"
+                            android:background="@drawable/cancel_button"/>
+
+                    <Button
+                            android:id="@+id/horizontal_allow_button"
+                            android:layout_width="54dp"
+                            android:layout_height="54dp"
+                            android:layout_gravity="top|right"
+                            android:layout_marginRight="16dp"
+                            android:layout_marginTop="9dp"
+                            app:layout_marginRightRound="36dp"
+                            app:layout_marginTopRound="12dp"
+                            android:background="@drawable/confirm_button"/>
+                </android.support.wearable.view.WearableFrameLayout>
+
+                <android.support.wearable.view.WearableFrameLayout
+                        android:id="@+id/vertical_button_bar"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:visibility="gone">
+                    <LinearLayout
+                            android:id="@+id/buttonPanel"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:orientation="vertical">
+                        <Button
+                                android:id="@+id/vertical_allow_button"
+                                style="@style/Widget.WearDiag.Button"/>
+
+                        <Button
+                                android:id="@+id/vertical_deny_button"
+                                style="@style/Widget.WearDiag.Button"/>
+
+                        <Button
+                                android:id="@+id/vertical_deny_do_not_ask_again_button"
+                                style="@style/Widget.WearDiag.Button"/>
+                    </LinearLayout>
+                </android.support.wearable.view.WearableFrameLayout>
+            </FrameLayout>
+        </LinearLayout>
+    </ScrollView>
+</FrameLayout>
\ No newline at end of file
diff --git a/res/values-watch/colors.xml b/res/values-watch/colors.xml
new file mode 100644
index 0000000..614e8f1
--- /dev/null
+++ b/res/values-watch/colors.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<resources>
+    <color name="grant_permissions_app_color">@color/grant_permissions_white_text_alpha_100</color>
+    <color name="grant_permissions_progress_color">@color/grant_permissions_white_text_alpha_100</color>
+    <color name="grant_permissions_title_color">@color/grant_permissions_white_text_alpha_70</color>
+
+    <color name="grant_permissions_white_text_alpha_100">@color/off_white</color>
+    <color name="grant_permissions_white_text_alpha_70">#b2eeeeee</color>
+
+    <color name="off_white">#ffeeeeee</color>
+</resources>
diff --git a/res/values-watch/dimens.xml b/res/values-watch/dimens.xml
new file mode 100644
index 0000000..29a65fd
--- /dev/null
+++ b/res/values-watch/dimens.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<resources>
+    <dimen name="grant_permissions_app_icon_size">32dp</dimen>
+    <dimen name="grant_permissions_app_icon_margin_top">8dp</dimen>
+
+    <dimen name="grant_permissions_app_breadcrumb_margin_bottom">3dp</dimen>
+
+    <dimen name="action_dialog_z">16dp</dimen>
+
+    <!-- Confirmation Dialog -->
+    <dimen name="conf_diag_floating_height">16dp</dimen>
+
+</resources>
diff --git a/res/values-watch/strings.xml b/res/values-watch/strings.xml
new file mode 100644
index 0000000..3ec72e5
--- /dev/null
+++ b/res/values-watch/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Title for the dialog button to deny a permission grant and never ask the user again. -->
+    <string name="grant_dialog_button_deny_dont_ask_again">Deny and don\'t ask again</string>
+
+    <!-- Template for the current permission from the total number of permissions. -->
+    <string name="current_permission_template">
+        <xliff:g id="current_permission_index" example="1">%1$s</xliff:g>
+        <xliff:g id="permission_count" example="2">%2$s</xliff:g>
+    </string>
+
+    <!-- Preference row title for showing system apps. -->
+    <string name="preference_show_system_apps">Show system apps</string>
+</resources>
diff --git a/res/values-watch/themes.xml b/res/values-watch/themes.xml
new file mode 100644
index 0000000..fd49185
--- /dev/null
+++ b/res/values-watch/themes.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2015 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
+  -->
+
+<resources>
+    <style name="Settings" parent="Theme.Leanback">
+        <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Permissions</item>
+    </style>
+
+    <style name="GrantPermissions" parent="Theme.Leanback">
+        <item name="android:windowIsFloating">true</item>
+        <item name="android:windowAnimationStyle">@style/Animation.Snackbar</item>
+        <item name="android:windowElevation">@dimen/action_dialog_z</item>
+    </style>
+
+    <style name="GrantPermissions.BreadcrumbText">
+        <item name="android:fontFamily">sans-serif-condensed</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:textColor">@color/grant_permissions_progress_color</item>
+    </style>
+
+    <style name="GrantPermissions.TitleText">
+        <item name="android:fontFamily">sans-serif-light</item>
+        <item name="android:textSize">18sp</item>
+        <item name="android:textColor">@color/grant_permissions_title_color</item>
+        <item name="android:lineSpacingMultiplier">1.221</item>
+    </style>
+
+    <style name="Animation.Snackbar" parent="@android:style/Animation">
+        <item name="android:windowEnterAnimation">@anim/snackbar_enter</item>
+        <item name="android:windowExitAnimation">@anim/snackbar_exit</item>
+    </style>
+</resources>
diff --git a/src/com/android/packageinstaller/permission/ui/GrantPermissionsActivity.java b/src/com/android/packageinstaller/permission/ui/GrantPermissionsActivity.java
index c451dd5..c55267e 100644
--- a/src/com/android/packageinstaller/permission/ui/GrantPermissionsActivity.java
+++ b/src/com/android/packageinstaller/permission/ui/GrantPermissionsActivity.java
@@ -73,6 +73,8 @@
 
         if (Utils.isTelevision(this)) {
             mViewHandler = new GrantPermissionsTvViewHandler(this).setResultListener(this);
+        } else if (isWatch()) {
+            mViewHandler = new GrantPermissionsWatchViewHandler(this).setResultListener(this);
         } else {
             mViewHandler = new GrantPermissionsDefaultViewHandler(this).setResultListener(this);
         }
@@ -357,6 +359,11 @@
         SafetyNetLogger.logPermissionsRequested(mAppPermissions.getPackageInfo(), groups);
     }
 
+    private boolean isWatch() {
+        PackageManager pm = getPackageManager();
+        return pm.hasSystemFeature(pm.FEATURE_WATCH);
+    }
+
     private static final class GroupState {
         static final int STATE_UNKNOWN = 0;
         static final int STATE_ALLOWED = 1;
diff --git a/src/com/android/packageinstaller/permission/ui/GrantPermissionsWatchViewHandler.java b/src/com/android/packageinstaller/permission/ui/GrantPermissionsWatchViewHandler.java
new file mode 100644
index 0000000..ac573c4
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/GrantPermissionsWatchViewHandler.java
@@ -0,0 +1,159 @@
+package com.android.packageinstaller.permission.ui;
+
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.android.packageinstaller.R;
+
+/**
+ * Watch-specific view handler for the grant permissions activity.
+ */
+final class GrantPermissionsWatchViewHandler extends PermissionConfirmationViewHandler
+        implements GrantPermissionsViewHandler {
+    private static final String TAG = "GrantPermissionsViewH";
+
+    private static final String ARG_GROUP_NAME = "ARG_GROUP_NAME";
+
+    private final Context mContext;
+
+    private ResultListener mResultListener;
+
+    private String mGroupName;
+    private boolean mShowDoNotAsk;
+
+    private CharSequence mMessage;
+    private String mCurrentPageText;
+    private Icon mIcon;
+
+    GrantPermissionsWatchViewHandler(Context context) {
+        super(context);
+        mContext = context;
+    }
+
+    @Override
+    public GrantPermissionsWatchViewHandler setResultListener(ResultListener listener) {
+        mResultListener = listener;
+        return this;
+    }
+
+    @Override
+    public View createView() {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "createView()");
+        }
+
+        mShowDoNotAsk = false;
+
+        return super.createView();
+    }
+
+    @Override
+    public void updateWindowAttributes(WindowManager.LayoutParams outLayoutParams) {
+        outLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
+        outLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
+        outLayoutParams.format = PixelFormat.OPAQUE;
+        outLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
+        outLayoutParams.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+    }
+
+    @Override
+    public void updateUi(String groupName, int groupCount, int groupIndex, Icon icon,
+            CharSequence message, boolean showDoNotAsk) {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "updateUi() - groupName: " + groupName
+                            + ", groupCount: " + groupCount
+                            + ", groupIndex: " + groupIndex
+                            + ", icon: " + icon
+                            + ", message: " + message
+                            + ", showDoNotAsk: " + showDoNotAsk);
+        }
+
+        mGroupName = groupName;
+        mShowDoNotAsk = showDoNotAsk;
+        mMessage = message;
+        mIcon = icon;
+        mCurrentPageText = (groupCount > 1 ?
+                mContext.getString(R.string.current_permission_template, groupIndex + 1, groupCount)
+                : null);
+
+        invalidate();
+    }
+
+    @Override
+    public void saveInstanceState(Bundle outState) {
+        outState.putString(ARG_GROUP_NAME, mGroupName);
+    }
+
+    @Override
+    public void loadInstanceState(Bundle savedInstanceState) {
+        mGroupName = savedInstanceState.getString(ARG_GROUP_NAME);
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (mResultListener != null) {
+            mResultListener.onPermissionGrantResult(mGroupName, false, false);
+        }
+    }
+
+    @Override // PermissionConfirmationViewHandler
+    public void onAllow() {
+        onClick(true /* granted */, false /* doNotAskAgain */);
+    }
+
+    @Override // PermissionConfirmationViewHandler
+    public void onDeny() {
+        onClick(false /* granted */, false /* doNotAskAgain */);
+    }
+
+    @Override // PermissionConfirmationViewHandler
+    public void onDenyDoNotAskAgain() {
+        onClick(false /* granted */, true /* doNotAskAgain */);
+    }
+
+    @Override // PermissionConfirmationViewHandler
+    public CharSequence getCurrentPageText() {
+        return mCurrentPageText;
+    }
+
+    @Override // PermissionConfirmationViewHandler
+    public Icon getPermissionIcon() {
+        return mIcon;
+    }
+
+    @Override // PermissionConfirmationViewHandler
+    public CharSequence getMessage() {
+        return mMessage;
+    }
+
+    @Override // PermissionConfirmationViewHandler
+    public int getButtonBarMode() {
+        return mShowDoNotAsk ? MODE_VERTICAL_BUTTONS : MODE_HORIZONTAL_BUTTONS;
+    }
+
+    @Override // PermissionConfirmationViewHandler
+    public CharSequence getVerticalAllowText() {
+        return mContext.getString(R.string.grant_dialog_button_allow);
+    }
+
+    @Override // PermissionConfirmationViewHandler
+    public CharSequence getVerticalDenyText() {
+        return mContext.getString(R.string.grant_dialog_button_deny);
+    }
+
+    @Override // PermissionConfirmationViewHandler
+    public CharSequence getVerticalDenyDoNotAskAgainText() {
+        return mContext.getString(R.string.grant_dialog_button_deny_dont_ask_again);
+    }
+
+    private void onClick(boolean granted, boolean doNotAskAgain) {
+        if (mResultListener != null) {
+            mResultListener.onPermissionGrantResult(mGroupName, granted, doNotAskAgain);
+        }
+    }
+}
diff --git a/src/com/android/packageinstaller/permission/ui/PermissionConfirmationViewHandler.java b/src/com/android/packageinstaller/permission/ui/PermissionConfirmationViewHandler.java
new file mode 100644
index 0000000..63ed0a4
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/PermissionConfirmationViewHandler.java
@@ -0,0 +1,149 @@
+package com.android.packageinstaller.permission.ui;
+
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import com.android.packageinstaller.R;
+
+public abstract class PermissionConfirmationViewHandler implements
+        View.OnClickListener {
+    public static final int MODE_HORIZONTAL_BUTTONS = 0;
+    public static final int MODE_VERTICAL_BUTTONS = 1;
+
+    private View mRoot;
+    private TextView mCurrentPageText;
+    private ImageView mIcon;
+    private TextView mMessage;
+    private ScrollView mScrollingContainer;
+    private ViewGroup mContent;
+    private ViewGroup mHorizontalButtonBar;
+    private ViewGroup mVerticalButtonBar;
+    private Button mVerticalAllow;
+    private Button mVerticalDeny;
+    private Button mVerticalDenyDoNotAskAgain;
+    private View mButtonBarContainer;
+
+    private Context mContext;
+
+    // TODO: Move these into a builder
+    public abstract void onAllow();
+    public abstract void onDeny();
+    public abstract void onDenyDoNotAskAgain();
+    public abstract CharSequence getVerticalAllowText();
+    public abstract CharSequence getVerticalDenyText();
+    public abstract CharSequence getVerticalDenyDoNotAskAgainText();
+    public abstract CharSequence getCurrentPageText();
+    public abstract Icon getPermissionIcon();
+    public abstract CharSequence getMessage();
+
+    public PermissionConfirmationViewHandler(Context context) {
+        mContext = context;
+    }
+
+    public View createView() {
+        mRoot = LayoutInflater.from(mContext).inflate(R.layout.grant_permissions, null);
+
+        mMessage = (TextView) mRoot.findViewById(R.id.message);
+        mCurrentPageText = (TextView) mRoot.findViewById(R.id.current_page_text);
+        mIcon = (ImageView) mRoot.findViewById(R.id.icon);
+        mButtonBarContainer = mRoot.findViewById(R.id.button_bar_container);
+        mContent = (ViewGroup) mRoot.findViewById(R.id.content);
+        mScrollingContainer = (ScrollView) mRoot.findViewById(R.id.scrolling_container);
+        mHorizontalButtonBar = (ViewGroup) mRoot.findViewById(R.id.horizontal_button_bar);
+        mVerticalButtonBar = (ViewGroup) mRoot.findViewById(R.id.vertical_button_bar);
+
+        Button horizontalAllow = (Button) mRoot.findViewById(R.id.horizontal_allow_button);
+        Button horizontalDeny = (Button) mRoot.findViewById(R.id.horizontal_deny_button);
+        horizontalAllow.setOnClickListener(this);
+        horizontalDeny.setOnClickListener(this);
+
+        mVerticalAllow = (Button) mRoot.findViewById(R.id.vertical_allow_button);
+        mVerticalDeny = (Button) mRoot.findViewById(R.id.vertical_deny_button);
+        mVerticalDenyDoNotAskAgain =
+                (Button) mRoot.findViewById(R.id.vertical_deny_do_not_ask_again_button);
+        mVerticalAllow.setOnClickListener(this);
+        mVerticalDeny.setOnClickListener(this);
+        mVerticalDenyDoNotAskAgain.setOnClickListener(this);
+
+        return mRoot;
+    }
+
+    /**
+     * Child class should override this for other modes.  Call invalidate() to update the UI to the
+     * new button mode.
+     * @return The current mode the layout should use for the buttons
+     */
+    public int getButtonBarMode() {
+        return MODE_HORIZONTAL_BUTTONS;
+    }
+
+    public void invalidate() {
+        CharSequence currentPageText = getCurrentPageText();
+        if (!TextUtils.isEmpty(currentPageText)) {
+            mCurrentPageText.setText(currentPageText);
+            mCurrentPageText.setVisibility(View.VISIBLE);
+        } else {
+            mCurrentPageText.setVisibility(View.INVISIBLE);
+        }
+
+        Icon icon = getPermissionIcon();
+        if (icon != null) {
+            mIcon.setImageIcon(icon);
+            mIcon.setVisibility(View.VISIBLE);
+        } else {
+            mIcon.setVisibility(View.INVISIBLE);
+        }
+
+        mMessage.setText(getMessage());
+
+        switch (getButtonBarMode()) {
+            case MODE_HORIZONTAL_BUTTONS:
+                mHorizontalButtonBar.setVisibility(View.VISIBLE);
+                mVerticalButtonBar.setVisibility(View.GONE);
+                break;
+            case MODE_VERTICAL_BUTTONS:
+                mHorizontalButtonBar.setVisibility(View.GONE);
+                mVerticalButtonBar.setVisibility(View.VISIBLE);
+                mVerticalAllow.setText(getVerticalAllowText());
+                mVerticalDeny.setText(getVerticalDenyText());
+                mVerticalDenyDoNotAskAgain.setText(getVerticalDenyDoNotAskAgainText());
+
+                mVerticalAllow.setCompoundDrawablesWithIntrinsicBounds(
+                        mContext.getDrawable(R.drawable.confirm_button), null, null, null);
+                mVerticalDeny.setCompoundDrawablesWithIntrinsicBounds(
+                        mContext.getDrawable(R.drawable.cancel_button), null, null, null);
+                mVerticalDenyDoNotAskAgain.setCompoundDrawablesWithIntrinsicBounds(
+                        mContext.getDrawable(R.drawable.cancel_button), null, null, null);
+                break;
+        }
+
+        mScrollingContainer.scrollTo(0, 0);
+    }
+
+    @Override
+    public void onClick(View v) {
+        int id = v.getId();
+        switch (id) {
+            case R.id.horizontal_allow_button:
+            case R.id.vertical_allow_button:
+                onAllow();
+                break;
+            case R.id.horizontal_deny_button:
+            case R.id.vertical_deny_button:
+                onDeny();
+                break;
+            case R.id.vertical_deny_do_not_ask_again_button:
+                onDenyDoNotAskAgain();
+                break;
+        }
+    }
+}