Merge "First pass for Dialer onboarding flow" into ub-contactsdialer-a-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index e58d2ce..94333eb 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -244,6 +244,12 @@
         <activity android:name="com.android.contacts.common.vcard.ExportVCardActivity"
                   android:theme="@style/BackgroundOnlyTheme"/>
 
+        <activity
+            android:name="com.android.dialer.onboard.OnboardingActivity"
+            android:theme="@style/OnboardingFlowTheme"
+            android:screenOrientation="nosensor"
+            android:exported="false" />
+
         <service
             android:name="com.android.contacts.common.vcard.VCardService"
             android:exported="false"/>
diff --git a/res/layout/onboarding_activity.xml b/res/layout/onboarding_activity.xml
new file mode 100644
index 0000000..a893ce4
--- /dev/null
+++ b/res/layout/onboarding_activity.xml
@@ -0,0 +1,21 @@
+<?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"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:id="@+id/onboarding_fragment_container">
+</FrameLayout>
diff --git a/res/layout/onboarding_screen_fragment.xml b/res/layout/onboarding_screen_fragment.xml
new file mode 100644
index 0000000..f4136ae
--- /dev/null
+++ b/res/layout/onboarding_screen_fragment.xml
@@ -0,0 +1,66 @@
+<?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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="20dp" >
+
+    <TextView
+        android:id="@+id/onboarding_screen_title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_above="@+id/onboarding_screen_content"
+        android:textAppearance="?android:attr/textAppearanceLarge"
+        android:textColor="@color/onboarding_primary_text_color" />
+
+    <TextView
+        android:id="@id/onboarding_screen_content"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_above="@+id/onboarding_buttons_container"
+        android:layout_marginTop="20dp"
+        android:textColor="@color/onboarding_primary_text_color" />
+
+    <LinearLayout
+        android:id="@id/onboarding_buttons_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_marginTop="20dp" >
+
+        <Button
+            android:id="@+id/onboard_skip_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:text="@string/onboarding_skip_button"
+            android:textColor="@color/onboarding_primary_text_color"
+            style="?android:attr/borderlessButtonStyle" />
+
+        <Button
+            android:id="@+id/onboard_next_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:text="@string/onboarding_next_button"
+            android:textColor="@color/onboarding_primary_text_color"
+            style="?android:attr/borderlessButtonStyle" />
+
+    </LinearLayout>
+</RelativeLayout>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index a747927..ad493e1 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -111,4 +111,9 @@
     <color name="delete_icon_tint">#6D6D6D</color>
     <color name="blocked_number_background">#E0E0E0</color>
     <color name="blocked_number_accent_color">#42A5F5</color>
+
+    <!-- Colors for onboarding flow -->
+    <color name="onboarding_primary_text_color">#ffffff</color>
+    <color name="onboarding_default_dialer_screen_background_color">#e06055</color>
+    <color name="onboarding_permissions_screen_background_color">#689f38</color>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d92471e..e5d32ea 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -884,4 +884,22 @@
 
     <!-- Shown as a message that notifies the user that the Phone app cannot write to system settings, which is why the system settings app is being launched directly instead.-->
     <string name="toast_cannot_write_system_settings">Phone app does not have permission to write to system settings.</string>
+
+    <!-- Title of the onboarding screen that asks the user to make Phone the default Phone app -->
+    <string name="request_default_dialer_screen_title">A better way of calling is calling</string>
+
+    <!-- Content of the onboarding screen that asks the user to make Phone the default Phone app -->
+    <string name="request_default_dialer_screen_content">Make Phone your default phone app to be able to do things like see who\'s calling you, even when they\'re not in your contacts.</string>
+
+    <!-- Title of the onboarding screen that asks the user to grant us the Contacts and Phone permissions -->
+    <string name="request_permissions_screen_title">Get talking to your friends and family</string>
+
+    <!-- Content of the onboarding screen that asks the user to grant us the Contacts and Phone permissions -->
+    <string name="request_permissions_screen_content">Phone will need to access your phone and contacts to make calls to people in your contacts.</string>
+
+    <!-- The label of the button used to skip a screen in the onboarding flow -->
+    <string name="onboarding_skip_button">Skip</string>
+
+    <!-- The label of the button used to go to the next screen in the onboarding flow -->
+    <string name="onboarding_next_button">Next</string>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 17b4a25..86c3ad4 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -109,6 +109,13 @@
         <item name="actionOverflowButtonStyle">@style/DialtactsActionBarOverflowWhite</item>
     </style>
 
+    <style name="OnboardingFlowTheme" parent="DialtactsThemeWithoutActionBarOverlay">
+        <item name="android:windowActionBar">false</item>
+        <item name="windowActionBar">false</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="windowNoTitle">true</item>
+    </style>
+
     <!-- Hide the actionbar title during the activity preview -->
     <style name="DialtactsActivityTheme" parent="DialtactsTheme">
         <!-- Styles that require AppCompat compatibility, remember to update both sets -->
diff --git a/src/com/android/dialer/onboard/OnboardingActivity.java b/src/com/android/dialer/onboard/OnboardingActivity.java
new file mode 100644
index 0000000..75378e9
--- /dev/null
+++ b/src/com/android/dialer/onboard/OnboardingActivity.java
@@ -0,0 +1,245 @@
+/*
+ * 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.
+ */
+
+package com.android.dialer.onboard;
+
+import static android.Manifest.permission.CALL_PHONE;
+import static android.Manifest.permission.READ_CONTACTS;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.telecom.TelecomManager;
+
+import com.android.contacts.common.util.PermissionsUtil;
+import com.android.dialer.TransactionSafeActivity;
+import com.android.dialer.onboard.OnboardingController.OnboardingScreen;
+import com.android.dialer.onboard.OnboardingController.OnboardingUi;
+import com.android.dialer.util.TelecomUtil;
+import com.android.dialer.R;
+
+/**
+ * Activity hosting the onboarding UX flow that appears when you launch Dialer and you don't have
+ * the necessary permissions to run the app.
+ */
+public class OnboardingActivity extends TransactionSafeActivity implements OnboardingUi,
+        PermissionsChecker, OnboardingFragment.HostInterface {
+    public static final String KEY_ALREADY_REQUESTED_DEFAULT_DIALER =
+            "key_already_requested_default_dialer";
+
+    public static final int SCREEN_DEFAULT_DIALER = 0;
+    public static final int SCREEN_PERMISSIONS = 1;
+    public static final int SCREEN_COUNT = 2;
+
+    private OnboardingController mOnboardingController;
+
+    private DefaultDialerOnboardingScreen mDefaultDialerOnboardingScreen;
+    private PermissionsOnboardingScreen mPermissionsOnboardingScreen;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.onboarding_activity);
+        mOnboardingController = new OnboardingController(this);
+        mDefaultDialerOnboardingScreen = new DefaultDialerOnboardingScreen(this);
+        mPermissionsOnboardingScreen = new PermissionsOnboardingScreen(this);
+        mOnboardingController.addScreen(mDefaultDialerOnboardingScreen);
+        mOnboardingController.addScreen(mPermissionsOnboardingScreen);
+
+        mOnboardingController.showNextScreen();
+    }
+
+    @Override
+    public void showScreen(int screenId) {
+        if (!isSafeToCommitTransactions()) {
+            return;
+        }
+        final Fragment fragment;
+        switch (screenId) {
+            case SCREEN_DEFAULT_DIALER:
+                fragment = mDefaultDialerOnboardingScreen.getFragment();
+                break;
+            case SCREEN_PERMISSIONS:
+                fragment = mPermissionsOnboardingScreen.getFragment();
+                break;
+            default:
+                return;
+        }
+
+        final FragmentTransaction ft = getFragmentManager().beginTransaction();
+        ft.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
+        ft.replace(R.id.onboarding_fragment_container, fragment);
+        ft.commit();
+    }
+
+    @Override
+    public void completeOnboardingFlow() {
+        final Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
+        editor.putBoolean(KEY_ALREADY_REQUESTED_DEFAULT_DIALER, true).apply();
+        finish();
+    }
+
+    @Override
+    public boolean hasPhonePermissions() {
+        return PermissionsUtil.hasPhonePermissions(this);
+    }
+
+    @Override
+    public boolean hasContactsPermissions() {
+        return PermissionsUtil.hasContactsPermissions(this);
+    }
+
+    @Override
+    public boolean isDefaultOrSystemDialer() {
+        return TelecomUtil.hasModifyPhoneStatePermission(this);
+    }
+
+    @Override
+    public boolean previouslyRequestedDefaultDialer() {
+        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+        return preferences.getBoolean(KEY_ALREADY_REQUESTED_DEFAULT_DIALER, false);
+    }
+
+    /**
+     * Triggers the screen-specific logic that should occur when the next button is clicked.
+     */
+    @Override
+    public void onNextClicked(int screenId) {
+        switch (screenId) {
+            case SCREEN_DEFAULT_DIALER:
+                mDefaultDialerOnboardingScreen.onNextClicked(this);
+                break;
+            case SCREEN_PERMISSIONS:
+                mPermissionsOnboardingScreen.onNextClicked(this);
+                break;
+            default:
+                return;
+        }
+    }
+
+    @Override
+    public void onSkipClicked(int screenId) {
+        mOnboardingController.onScreenResult(screenId, false);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == SCREEN_DEFAULT_DIALER
+                && resultCode == RESULT_OK) {
+            mOnboardingController.onScreenResult(SCREEN_DEFAULT_DIALER, true);
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions,
+            int[] grantResults) {
+        boolean allPermissionsGranted = true;
+        if (requestCode == SCREEN_PERMISSIONS) {
+            if (permissions.length == 0 && grantResults.length == 0) {
+                // Cancellation of permissions dialog
+                allPermissionsGranted = false;
+            } else {
+                for (int result : grantResults) {
+                    if (result == PackageManager.PERMISSION_DENIED) {
+                        allPermissionsGranted = false;
+                    }
+                }
+            }
+
+            if (allPermissionsGranted) {
+                mOnboardingController.onScreenResult(SCREEN_PERMISSIONS, true);
+            }
+        }
+    }
+
+    public static class DefaultDialerOnboardingScreen extends OnboardingScreen {
+        private PermissionsChecker mPermissionsChecker;
+
+        public DefaultDialerOnboardingScreen(PermissionsChecker permissionsChecker) {
+            mPermissionsChecker = permissionsChecker;
+        }
+
+        @Override
+        public boolean shouldShowScreen() {
+            return !mPermissionsChecker.previouslyRequestedDefaultDialer()
+                    && !mPermissionsChecker.isDefaultOrSystemDialer();
+        }
+
+        @Override
+        public boolean canSkipScreen() {
+            return true;
+        }
+
+        public Fragment getFragment() {
+            return new OnboardingFragment(
+                    SCREEN_DEFAULT_DIALER,
+                    canSkipScreen(),
+                    R.color.onboarding_default_dialer_screen_background_color,
+                    R.string.request_default_dialer_screen_title,
+                    R.string.request_default_dialer_screen_content
+            );
+        }
+
+        @Override
+        public void onNextClicked(Activity activity) {
+            final Intent intent = new Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER);
+            intent.putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME,
+                    activity.getPackageName());
+            activity.startActivityForResult(intent, SCREEN_DEFAULT_DIALER);
+        }
+    }
+
+    public static class PermissionsOnboardingScreen extends OnboardingScreen {
+        private PermissionsChecker mPermissionsChecker;
+
+        public PermissionsOnboardingScreen(PermissionsChecker permissionsChecker) {
+            mPermissionsChecker = permissionsChecker;
+        }
+
+        @Override
+        public boolean shouldShowScreen() {
+            return !(mPermissionsChecker.hasPhonePermissions()
+                    && mPermissionsChecker.hasContactsPermissions());
+        }
+
+        @Override
+        public boolean canSkipScreen() {
+            return false;
+        }
+
+        public Fragment getFragment() {
+            return new OnboardingFragment(
+                    SCREEN_PERMISSIONS,
+                    canSkipScreen(),
+                    R.color.onboarding_permissions_screen_background_color,
+                    R.string.request_permissions_screen_title,
+                    R.string.request_permissions_screen_content
+            );
+        }
+
+        @Override
+        public void onNextClicked(Activity activity) {
+            activity.requestPermissions(new String[] {CALL_PHONE, READ_CONTACTS},
+                    SCREEN_PERMISSIONS);
+        }
+    }
+}
diff --git a/src/com/android/dialer/onboard/OnboardingController.java b/src/com/android/dialer/onboard/OnboardingController.java
new file mode 100644
index 0000000..f799479
--- /dev/null
+++ b/src/com/android/dialer/onboard/OnboardingController.java
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+package com.android.dialer.onboard;
+
+import android.app.Activity;
+
+import java.util.ArrayList;
+
+/**
+ * Class that manages the display of various fragments that show the user prompts asking for
+ * certain privileged positions.
+ */
+public class OnboardingController {
+    public static abstract class OnboardingScreen {
+        public abstract boolean shouldShowScreen();
+        public abstract boolean canSkipScreen();
+        public abstract void onNextClicked(Activity activity);
+    }
+
+    public interface OnboardingUi {
+        public void showScreen(int screenId);
+        /**
+         * Called when all the necessary permissions have been granted and the main activity
+         * can launch.
+         */
+        public void completeOnboardingFlow();
+    }
+
+    private int mCurrentScreen = -1;
+    private OnboardingUi mOnboardingUi;
+    private ArrayList<OnboardingScreen> mScreens = new ArrayList<> ();
+
+    public OnboardingController(OnboardingUi onBoardingUi) {
+        mOnboardingUi = onBoardingUi;
+    }
+
+    public void addScreen(OnboardingScreen screen) {
+        mScreens.add(screen);
+    }
+
+    public void showNextScreen() {
+        mCurrentScreen++;
+
+        if (mCurrentScreen >= mScreens.size()) {
+            // Reached the end of onboarding flow
+            mOnboardingUi.completeOnboardingFlow();
+            return;
+        }
+
+        if (mScreens.get(mCurrentScreen).shouldShowScreen()) {
+            mOnboardingUi.showScreen(mCurrentScreen);
+        } else {
+            showNextScreen();
+        }
+    }
+
+    public void onScreenResult(int screenId, boolean success) {
+        if (screenId >= mScreens.size()) {
+            return;
+        }
+
+        // Show the next screen in the onboarding flow only under the following situations:
+        // 1) Success was indicated, and the
+        // 2) The user tried to skip the screen, and the screen can be skipped
+        if (success && !mScreens.get(mCurrentScreen).shouldShowScreen()) {
+            showNextScreen();
+        } else if (mScreens.get(mCurrentScreen).canSkipScreen()) {
+            showNextScreen();
+        }
+    }
+}
diff --git a/src/com/android/dialer/onboard/OnboardingFragment.java b/src/com/android/dialer/onboard/OnboardingFragment.java
new file mode 100644
index 0000000..77b265b
--- /dev/null
+++ b/src/com/android/dialer/onboard/OnboardingFragment.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package com.android.dialer.onboard;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.dialer.R;
+
+public class OnboardingFragment extends Fragment implements OnClickListener {
+    public static final String ARG_SCREEN_ID = "arg_screen_id";
+    public static final String ARG_CAN_SKIP_SCREEN = "arg_can_skip_screen";
+    public static final String ARG_BACKGROUND_COLOR_RESOURCE = "arg_background_color";
+    public static final String ARG_TEXT_TITLE_RESOURCE = "arg_text_title_resource";
+    public static final String ARG_TEXT_CONTENT_RESOURCE = "arg_text_content_resource";
+
+    private int mScreenId;
+
+    public interface HostInterface {
+        public void onNextClicked(int screenId);
+        public void onSkipClicked(int screenId);
+    }
+
+    public OnboardingFragment() {}
+
+    public OnboardingFragment(int screenId, boolean canSkipScreen, int backgroundColorResourceId,
+            int textTitleResourceId, int textContentResourceId) {
+        final Bundle args = new Bundle();
+        args.putInt(ARG_SCREEN_ID, screenId);
+        args.putBoolean(ARG_CAN_SKIP_SCREEN, canSkipScreen);
+        args.putInt(ARG_BACKGROUND_COLOR_RESOURCE, backgroundColorResourceId);
+        args.putInt(ARG_TEXT_TITLE_RESOURCE, textTitleResourceId);
+        args.putInt(ARG_TEXT_CONTENT_RESOURCE, textContentResourceId);
+        setArguments(args);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mScreenId = getArguments().getInt(ARG_SCREEN_ID);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        final View view = inflater.inflate(R.layout.onboarding_screen_fragment, container, false);
+        view.setBackgroundColor(getResources().getColor(
+                getArguments().getInt(ARG_BACKGROUND_COLOR_RESOURCE), null));
+        ((TextView) view.findViewById(R.id.onboarding_screen_content)).
+                setText(getArguments().getInt(ARG_TEXT_CONTENT_RESOURCE));
+        ((TextView) view.findViewById(R.id.onboarding_screen_title)).
+        setText(getArguments().getInt(ARG_TEXT_TITLE_RESOURCE));
+        if (!getArguments().getBoolean(ARG_CAN_SKIP_SCREEN)) {
+            view.findViewById(R.id.onboard_skip_button).setVisibility(View.INVISIBLE);
+        }
+
+        view.findViewById(R.id.onboard_skip_button).setOnClickListener(this);
+        view.findViewById(R.id.onboard_next_button).setOnClickListener(this);
+
+        return view;
+    }
+
+    int getScreenId() {
+        return mScreenId;
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (v.getId() == R.id.onboard_skip_button) {
+            ((HostInterface) getActivity()).onSkipClicked(getScreenId());
+        } else if (v.getId() == R.id.onboard_next_button) {
+            ((HostInterface) getActivity()).onNextClicked(getScreenId());
+        }
+    }
+}
diff --git a/src/com/android/dialer/onboard/PermissionsChecker.java b/src/com/android/dialer/onboard/PermissionsChecker.java
new file mode 100644
index 0000000..78d175e
--- /dev/null
+++ b/src/com/android/dialer/onboard/PermissionsChecker.java
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+package com.android.dialer.onboard;
+
+/**
+ * Defines a mockable interface used to verify whether certain permissions/privileged operations
+ * are possible.
+ */
+public interface PermissionsChecker {
+    public boolean hasPhonePermissions();
+    public boolean hasContactsPermissions();
+    public boolean isDefaultOrSystemDialer();
+    public boolean previouslyRequestedDefaultDialer();
+}
diff --git a/tests/Android.mk b/tests/Android.mk
index 21beca8..07f4f00 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -16,6 +16,9 @@
 # Include all test java files.
 LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs))
 
+LOCAL_STATIC_JAVA_LIBRARIES += \
+        mockito-target
+
 LOCAL_PACKAGE_NAME := DialerTests
 
 LOCAL_INSTRUMENTATION_FOR := Dialer
diff --git a/tests/src/com/android/dialer/onboard/DefaultDialerOnboardScreenTest.java b/tests/src/com/android/dialer/onboard/DefaultDialerOnboardScreenTest.java
new file mode 100644
index 0000000..f9724c1
--- /dev/null
+++ b/tests/src/com/android/dialer/onboard/DefaultDialerOnboardScreenTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+package com.android.dialer.onboard;
+
+import static org.mockito.Mockito.when;
+
+import android.test.AndroidTestCase;
+
+import com.android.dialer.onboard.OnboardingActivity.DefaultDialerOnboardingScreen;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class DefaultDialerOnboardScreenTest extends AndroidTestCase {
+    private DefaultDialerOnboardingScreen mScreen;
+    @Mock private PermissionsChecker mPermissionsChecker;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+        mScreen = new DefaultDialerOnboardingScreen(mPermissionsChecker);
+    }
+
+    public void testNeverRequestedForDefaultDialer_shouldShowScreen() {
+        when(mPermissionsChecker.previouslyRequestedDefaultDialer()).thenReturn(false);
+        assertTrue(mScreen.shouldShowScreen());
+    }
+
+    public void testAlreadyAskedForDefaultDialer_shouldNotShowScreen() {
+        when(mPermissionsChecker.previouslyRequestedDefaultDialer()).thenReturn(true);
+        assertFalse(mScreen.shouldShowScreen());
+    }
+
+    public void testAlreadySetAsDefaultDialer_shouldNotShowScreen() {
+        when(mPermissionsChecker.previouslyRequestedDefaultDialer()).thenReturn(false);
+        when(mPermissionsChecker.isDefaultOrSystemDialer()).thenReturn(true);
+        assertFalse(mScreen.shouldShowScreen());
+    }
+
+    public void testCanSkipScreen() {
+        assertTrue(mScreen.canSkipScreen());
+    }
+}
diff --git a/tests/src/com/android/dialer/onboard/OnboardingControllerTest.java b/tests/src/com/android/dialer/onboard/OnboardingControllerTest.java
new file mode 100644
index 0000000..d9c8ce5
--- /dev/null
+++ b/tests/src/com/android/dialer/onboard/OnboardingControllerTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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.
+ */
+package com.android.dialer.onboard;
+
+import android.app.Activity;
+import android.test.AndroidTestCase;
+
+public class OnboardingControllerTest extends AndroidTestCase {
+    private MockOnboardUi mOnboardUi;
+    private OnboardingController mController;
+
+    public class MockOnboardUi implements OnboardingController.OnboardingUi {
+        public int currentScreen = -1;
+        public boolean completedOnboardingFlow = false;
+
+        @Override
+        public void showScreen(int screenId) {
+            currentScreen = screenId;
+        }
+
+        @Override
+        public void completeOnboardingFlow() {
+            completedOnboardingFlow = true;
+        }
+    }
+
+    public class MockScreen extends OnboardingController.OnboardingScreen {
+        boolean shouldShowScreen;
+        boolean canSkipScreen;
+
+        public MockScreen(boolean shouldShowScreen, boolean canSkipScreen) {
+            this.shouldShowScreen = shouldShowScreen;
+            this.canSkipScreen = canSkipScreen;
+        }
+
+        @Override
+        public boolean shouldShowScreen() {
+            return shouldShowScreen;
+        }
+
+        @Override
+        public boolean canSkipScreen() {
+            return canSkipScreen;
+        }
+
+        @Override
+        public void onNextClicked(Activity activity) {
+        }
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mOnboardUi = new MockOnboardUi();
+        mController = new OnboardingController(mOnboardUi);
+    }
+
+    public void testNoScreensToDisplay_OnboardingFlowImmediatelyCompleted() {
+        mController.showNextScreen();
+        assertEquals(-1, mOnboardUi.currentScreen);
+        assertTrue(mOnboardUi.completedOnboardingFlow);
+    }
+
+    public void testSkipAllScreens_OnboardingFlowImmediatelyCompleted() {
+        mController.addScreen(new MockScreen(false /* shouldShowScreen */,
+                true /* canSkipScreen */));
+        mController.addScreen(new MockScreen(false /* shouldShowScreen */,
+                true /* canSkipScreen */));
+        mController.addScreen(new MockScreen(false /* shouldShowScreen */,
+                true /* canSkipScreen */));
+        mController.showNextScreen();
+        assertEquals(-1, mOnboardUi.currentScreen);
+        assertTrue(mOnboardUi.completedOnboardingFlow);
+    }
+
+    public void testFirstScreenNotNeeded_ShowsSecondScreen() {
+        mController.addScreen(new MockScreen(false /* shouldShowScreen */,
+                false /* canSkipScreen */));
+        mController.addScreen(new MockScreen(true /* shouldShowScreen */,
+                false /* canSkipScreen */));
+        mController.showNextScreen();
+        assertEquals(1, mOnboardUi.currentScreen);
+    }
+
+    public void testScreenRequired() {
+        final MockScreen mockScreen =
+                new MockScreen(true /* shouldShowScreen */, false /* canSkipScreen */);
+        mController.addScreen(mockScreen);
+
+        mController.showNextScreen();
+        assertEquals(0, mOnboardUi.currentScreen);
+        assertFalse(mOnboardUi.completedOnboardingFlow);
+
+        // User tried to skip an unskippable screen
+        mController.onScreenResult(0, false);
+        assertEquals(0, mOnboardUi.currentScreen);
+        assertFalse(mOnboardUi.completedOnboardingFlow);
+
+        // User said yes, but the underlying requirements have not been fulfilled yet, so don't
+        // show the next screen. Should be very rare in practice.
+        mController.onScreenResult(0, true);
+        assertEquals(0, mOnboardUi.currentScreen);
+        assertFalse(mOnboardUi.completedOnboardingFlow);
+
+        // Requirement has been fulfiled.
+        mockScreen.shouldShowScreen = false;
+        mController.onScreenResult(0, true);
+        assertTrue(mOnboardUi.completedOnboardingFlow);
+    }
+
+    /**
+     * Verifies the use case where completing the first screen will provide the necessary conditions
+     * to skip the second screen as well.
+     *
+     * For example, setting the default dialer in the first screen will automatically grant
+     * permissions such that the second permissions screen is no longer needed.
+     */
+    public void testFirstScreenCompleted_SkipsSecondScreen() {
+        final MockScreen mockScreen1 =
+                new MockScreen(true /* shouldShowScreen */, true /* canSkipScreen */);
+        final MockScreen mockScreen2 =
+                new MockScreen(true /* shouldShowScreen */, false /* canSkipScreen */);
+        mController.addScreen(mockScreen1);
+        mController.addScreen(mockScreen2);
+
+        mController.showNextScreen();
+        assertEquals(0, mOnboardUi.currentScreen);
+        assertFalse(mOnboardUi.completedOnboardingFlow);
+
+        // Screen 1 succeeded, screen 2 is no longer necessary
+        mockScreen2.shouldShowScreen = false;
+        mController.onScreenResult(0, true);
+        assertEquals(0, mOnboardUi.currentScreen);
+        assertTrue(mOnboardUi.completedOnboardingFlow);
+    }
+
+    /**
+     * Verifies the use case where skipping the first screen will proceed to show the second screen
+     * since the necessary conditions to skip the second screen have not been met.
+     */
+    public void testFirstScreenSkipped_ShowsSecondScreen() {
+        final MockScreen mockScreen1 =
+                new MockScreen(true /* shouldShowScreen */, true /* canSkipScreen */);
+        final MockScreen mockScreen2 =
+                new MockScreen(true /* shouldShowScreen */, false /* canSkipScreen */);
+        mController.addScreen(mockScreen1);
+        mController.addScreen(mockScreen2);
+
+        mController.showNextScreen();
+        assertEquals(0, mOnboardUi.currentScreen);
+        assertFalse(mOnboardUi.completedOnboardingFlow);
+
+        // Screen 1 skipped
+        mController.onScreenResult(0, false);
+        assertEquals(1, mOnboardUi.currentScreen);
+        assertFalse(mOnboardUi.completedOnboardingFlow);
+
+        // Repeatedly trying to skip screen 2 will not work since it is marked as unskippable.
+        mController.onScreenResult(1, false);
+        assertEquals(1, mOnboardUi.currentScreen);
+        assertFalse(mOnboardUi.completedOnboardingFlow);
+    }
+}
diff --git a/tests/src/com/android/dialer/onboard/PermissionsOnboardScreenTest.java b/tests/src/com/android/dialer/onboard/PermissionsOnboardScreenTest.java
new file mode 100644
index 0000000..ff5e3d5
--- /dev/null
+++ b/tests/src/com/android/dialer/onboard/PermissionsOnboardScreenTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package com.android.dialer.onboard;
+
+import static org.mockito.Mockito.when;
+
+import android.test.AndroidTestCase;
+
+import com.android.dialer.onboard.OnboardingActivity.PermissionsOnboardingScreen;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class PermissionsOnboardScreenTest extends AndroidTestCase {
+    private PermissionsOnboardingScreen mScreen;
+    @Mock private PermissionsChecker mPermissionsChecker;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+        mScreen = new PermissionsOnboardingScreen(mPermissionsChecker);
+    }
+
+    public void testMissingContactsAndPhonePermissions_shouldShowScreen() {
+        when(mPermissionsChecker.hasContactsPermissions()).thenReturn(false);
+        when(mPermissionsChecker.hasPhonePermissions()).thenReturn(false);
+        assertTrue(mScreen.shouldShowScreen());
+    }
+
+    public void testMissingContactsPermission_shouldShowScreen() {
+        when(mPermissionsChecker.hasContactsPermissions()).thenReturn(false);
+        when(mPermissionsChecker.hasPhonePermissions()).thenReturn(true);
+        assertTrue(mScreen.shouldShowScreen());
+    }
+
+    public void testMissingPhonePermission_shouldShowScreen() {
+        when(mPermissionsChecker.hasContactsPermissions()).thenReturn(true);
+        when(mPermissionsChecker.hasPhonePermissions()).thenReturn(false);
+        assertTrue(mScreen.shouldShowScreen());
+    }
+
+    public void testHasAllPermissions_shouldNotShowScreen() {
+        when(mPermissionsChecker.hasContactsPermissions()).thenReturn(true);
+        when(mPermissionsChecker.hasPhonePermissions()).thenReturn(true);
+        assertFalse(mScreen.shouldShowScreen());
+    }
+
+    public void testCanSkipScreen() {
+        assertFalse(mScreen.canSkipScreen());
+    }
+}