From 545a779f0df2825067679bee09bc966ca12b5974 Mon Sep 17 00:00:00 2001 From: Yorke Lee Date: Tue, 29 Sep 2015 15:02:34 -0700 Subject: First pass for Dialer onboarding flow * Add OnboardingActivity that controls the onboarding the UI ensuring that the user grants the necessary permissions before the Dialer can start. * Add first pass (no graphics, eyeballed measurements) for the screens that request for default dialer as well as permissions * OnboardingActivity is not actually launched at this moment - will be tied in to the various Dialer activities in a follow up CL. * Add tests for logic that controls the display of the screens in anticipation of future additions to the onboarding flow. * Add mockito library to DialerTests's Android.mk Bug: 24270592 Change-Id: I00d0f75edaecaa85042b136b0d830b5fbb3a0a73 --- .../android/dialer/onboard/OnboardingActivity.java | 245 +++++++++++++++++++++ .../dialer/onboard/OnboardingController.java | 85 +++++++ .../android/dialer/onboard/OnboardingFragment.java | 93 ++++++++ .../android/dialer/onboard/PermissionsChecker.java | 27 +++ 4 files changed, 450 insertions(+) create mode 100644 src/com/android/dialer/onboard/OnboardingActivity.java create mode 100644 src/com/android/dialer/onboard/OnboardingController.java create mode 100644 src/com/android/dialer/onboard/OnboardingFragment.java create mode 100644 src/com/android/dialer/onboard/PermissionsChecker.java (limited to 'src') diff --git a/src/com/android/dialer/onboard/OnboardingActivity.java b/src/com/android/dialer/onboard/OnboardingActivity.java new file mode 100644 index 000000000..75378e99d --- /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 000000000..f799479ed --- /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 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 000000000..77b265b2c --- /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 000000000..78d175e6f --- /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(); +} -- cgit v1.2.3