From 38019af70eb1ca084d36291390bbc54dc81027de Mon Sep 17 00:00:00 2001 From: Yorke Lee Date: Tue, 14 Jul 2015 17:05:38 -0700 Subject: Update Dialer UI to prompt for permissions First pass at adding functionality to Dialer to prompt for permissions. Also updates the UI for various fragments when there are no contacts/calls available to unify the UI with the new UI for the permissions denied state. * Refactor existing empty view functionality and encapsulate into a new custom view class * Add action buttons to existing empty views in various fragments -> Speed Dial (Add to favorites) - scrolls to all contacts fragment -> Recents (Make a call) - shows dialpad -> All contacts (Add a contact) - Launches add contact dialog * Add functionality to various fragments to detect if permissions are denied, and request the permission when the action button is pressed. -> Speed Dial (request for contacts permission) -> Call Log (request for phone permission) -> All contacts (request for contacts permission) * Remove now unneeded EmptyContactsListAdapter Remaining issues (to be addressed in a follow up CL to avoid bloating this CL): UI alignment tweaks for empty view to match mocks If the read contacts permission is requested from the speed dial screen, the all contacts fragment doesn't update with the list of contacts until restarted. Bug: 22174668 Change-Id: I70721914bb9b32910d746de288ccac049749e42e --- src/com/android/dialer/DialtactsActivity.java | 21 ++++- .../android/dialer/calllog/CallLogFragment.java | 95 ++++++++++++++++--- .../android/dialer/list/AllContactsFragment.java | 60 ++++++++++-- .../dialer/list/EmptyContactsListAdapter.java | 47 ---------- src/com/android/dialer/list/SpeedDialFragment.java | 37 +++++++- src/com/android/dialer/util/DialerUtils.java | 22 +---- .../android/dialer/widget/EmptyContentView.java | 102 +++++++++++++++++++++ 7 files changed, 289 insertions(+), 95 deletions(-) delete mode 100644 src/com/android/dialer/list/EmptyContactsListAdapter.java create mode 100644 src/com/android/dialer/widget/EmptyContentView.java (limited to 'src/com/android') diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java index 31d1b94bf..85197a530 100644 --- a/src/com/android/dialer/DialtactsActivity.java +++ b/src/com/android/dialer/DialtactsActivity.java @@ -66,6 +66,7 @@ import com.android.contacts.common.util.PermissionsUtil; import com.android.contacts.common.widget.FloatingActionButtonController; import com.android.contacts.commonbind.analytics.AnalyticsUtil; import com.android.dialer.calllog.CallLogActivity; +import com.android.dialer.calllog.CallLogFragment; import com.android.dialer.database.DialerDatabaseHelper; import com.android.dialer.dialpad.DialpadFragment; import com.android.dialer.dialpad.SmartDialNameMatcher; @@ -101,6 +102,7 @@ import java.util.List; public class DialtactsActivity extends TransactionSafeActivity implements View.OnClickListener, DialpadFragment.OnDialpadQueryChangedListener, OnListFragmentScrolledListener, + CallLogFragment.HostInterface, ListsFragment.HostInterface, SpeedDialFragment.HostInterface, SearchFragment.HostInterface, @@ -1207,6 +1209,24 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O mListsFragment.getRemoveView().setDragDropController(dragController); } + /** + * Implemented to satisfy {@link SpeedDialFragment.HostInterface} + */ + @Override + public void showAllContactsTab() { + if (mListsFragment != null) { + mListsFragment.showTab(ListsFragment.TAB_INDEX_ALL_CONTACTS); + } + } + + /** + * Implemented to satisfy {@link CallLogFragment.HostInterface} + */ + @Override + public void showDialpad() { + showDialpadFragment(true); + } + @Override public void onPickPhoneNumberAction(Uri dataUri) { // Specify call-origin so that users will see the previous tab instead of @@ -1322,7 +1342,6 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O return mActionBarHeight; } - private int getFabAlignment() { if (!mIsLandscape && !isInSearchUi() && mListsFragment.getCurrentTabIndex() == ListsFragment.TAB_INDEX_SPEED_DIAL) { diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java index 5d7c408ce..d1a9827eb 100644 --- a/src/com/android/dialer/calllog/CallLogFragment.java +++ b/src/com/android/dialer/calllog/CallLogFragment.java @@ -16,6 +16,8 @@ package com.android.dialer.calllog; +import static android.Manifest.permission.READ_CALL_LOG; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; @@ -26,6 +28,7 @@ import android.app.KeyguardManager; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.database.ContentObserver; import android.database.Cursor; import android.graphics.Rect; @@ -46,6 +49,7 @@ import android.widget.ListView; import android.widget.TextView; import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.util.PermissionsUtil; import com.android.contacts.common.util.ViewUtil; import com.android.dialer.R; import com.android.dialer.list.ListsFragment.HostInterface; @@ -55,6 +59,8 @@ import com.android.dialer.voicemail.VoicemailPlaybackPresenter; import com.android.dialer.voicemail.VoicemailStatusHelper; import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage; import com.android.dialer.voicemail.VoicemailStatusHelperImpl; +import com.android.dialer.widget.EmptyContentView; +import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; import com.android.dialerbind.ObjectFactory; import java.util.List; @@ -63,8 +69,8 @@ import java.util.List; * Displays a list of call log entries. To filter for a particular kind of call * (all, missed or voicemails), specify it in the constructor. */ -public class CallLogFragment extends Fragment - implements CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher { +public class CallLogFragment extends Fragment implements CallLogQueryHandler.Listener, + CallLogAdapter.CallFetcher, OnEmptyViewActionButtonClickedListener { private static final String TAG = "CallLogFragment"; /** @@ -81,6 +87,8 @@ public class CallLogFragment extends Fragment // No date-based filtering. private static final int NO_DATE_LIMIT = 0; + private static final int READ_CALL_LOG_PERMISSION_REQUEST_CODE = 1; + private RecyclerView mRecyclerView; private LinearLayoutManager mLayoutManager; private CallLogAdapter mAdapter; @@ -91,7 +99,7 @@ public class CallLogFragment extends Fragment /** Whether there is at least one voicemail source installed. */ private boolean mVoicemailSourcesAvailable = false; - private View mEmptyListView; + private EmptyContentView mEmptyListView; private KeyguardManager mKeyguardManager; private boolean mEmptyLoaderRunning; @@ -116,6 +124,8 @@ public class CallLogFragment extends Fragment private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver(); private boolean mRefreshDataRequired = true; + private boolean mHasReadCallLogPermission = false; + // Exactly same variable is in Fragment as a package private. private boolean mMenuVisible = true; @@ -130,6 +140,16 @@ public class CallLogFragment extends Fragment // the date filter are included. If zero, no date-based filtering occurs. private long mDateLimit = NO_DATE_LIMIT; + /* + * True if this instance of the CallLogFragment is the Recents screen shown in + * DialtactsActivity. + */ + private boolean mIsRecentsFragment; + + public interface HostInterface { + public void showDialpad(); + } + public CallLogFragment() { this(CallLogQueryHandler.CALL_TYPE_ALL, NO_LOG_LIMIT); } @@ -139,9 +159,7 @@ public class CallLogFragment extends Fragment } public CallLogFragment(int filterType, int logLimit) { - super(); - mCallTypeFilter = filterType; - mLogLimit = logLimit; + this(filterType, logLimit, NO_DATE_LIMIT); } /** @@ -162,7 +180,8 @@ public class CallLogFragment extends Fragment * @param dateLimit limits results to calls occurring on or after the specified date. */ public CallLogFragment(int filterType, int logLimit, long dateLimit) { - this(filterType, logLimit); + mCallTypeFilter = filterType; + mLogLimit = logLimit; mDateLimit = dateLimit; } @@ -175,6 +194,8 @@ public class CallLogFragment extends Fragment mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit); } + mIsRecentsFragment = mLogLimit != NO_LOG_LIMIT; + final Activity activity = getActivity(); final ContentResolver resolver = activity.getContentResolver(); String currentCountryIso = GeoUtil.getCurrentCountryIso(activity); @@ -268,7 +289,9 @@ public class CallLogFragment extends Fragment mRecyclerView.setHasFixedSize(true); mLayoutManager = new LinearLayoutManager(getActivity()); mRecyclerView.setLayoutManager(mLayoutManager); - mEmptyListView = view.findViewById(R.id.empty_list_view); + mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view); + mEmptyListView.setImage(R.drawable.empty_call_log); + mEmptyListView.setActionClickedListener(this); String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); boolean isShowingRecentsTab = mLogLimit != NO_LOG_LIMIT || mDateLimit != NO_DATE_LIMIT; @@ -287,7 +310,6 @@ public class CallLogFragment extends Fragment @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - updateEmptyMessage(mCallTypeFilter); mAdapter.onRestoreInstanceState(savedInstanceState); } @@ -305,6 +327,16 @@ public class CallLogFragment extends Fragment @Override public void onResume() { super.onResume(); + final boolean hasReadCallLogPermission = + PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG); + if (!mHasReadCallLogPermission && hasReadCallLogPermission) { + // We didn't have the permission before, and now we do. Force a refresh of the call log. + // Note that this code path always happens on a fresh start, but mRefreshDataRequired + // is already true in that case anyway. + mRefreshDataRequired = true; + updateEmptyMessage(mCallTypeFilter); + } + mHasReadCallLogPermission = hasReadCallLogPermission; refreshData(); } @@ -359,6 +391,17 @@ public class CallLogFragment extends Fragment } private void updateEmptyMessage(int filterType) { + final Context context = getActivity(); + if (context == null) { + return; + } + + if (!PermissionsUtil.hasPermission(context, READ_CALL_LOG)) { + mEmptyListView.setDescription(R.string.permission_no_calllog); + mEmptyListView.setActionLabel(R.string.permission_single_turn_on); + return; + } + final int messageId; switch (filterType) { case Calls.MISSED_TYPE: @@ -374,8 +417,12 @@ public class CallLogFragment extends Fragment throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: " + filterType); } - DialerUtils.configureEmptyListView( - mEmptyListView, R.drawable.empty_call_log, messageId, getResources()); + mEmptyListView.setDescription(messageId); + if (mIsRecentsFragment) { + mEmptyListView.setActionLabel(R.string.recentCalls_empty_action); + } else { + mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL); + } } CallLogAdapter getAdapter() { @@ -437,4 +484,30 @@ public class CallLogFragment extends Fragment CallLogNotificationsHelper.updateVoicemailNotifications(getActivity()); } } + + @Override + public void onEmptyViewActionButtonClicked(String[] permissions) { + final Activity activity = getActivity(); + if (activity == null) { + return; + } + + if (!PermissionsUtil.hasPermission(activity, READ_CALL_LOG)) { + requestPermissions(new String[] {READ_CALL_LOG}, READ_CALL_LOG_PERMISSION_REQUEST_CODE); + } else if (mIsRecentsFragment) { + // Show dialpad if we are the recents fragment. + ((HostInterface) activity).showDialpad(); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, + int[] grantResults) { + if (requestCode == READ_CALL_LOG_PERMISSION_REQUEST_CODE) { + if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) { + // Force a refresh of the data since we were missing the permission before this. + mRefreshDataRequired = true; + } + } + } } diff --git a/src/com/android/dialer/list/AllContactsFragment.java b/src/com/android/dialer/list/AllContactsFragment.java index d34250b48..00b629c2b 100644 --- a/src/com/android/dialer/list/AllContactsFragment.java +++ b/src/com/android/dialer/list/AllContactsFragment.java @@ -16,7 +16,11 @@ package com.android.dialer.list; +import static android.Manifest.permission.READ_CONTACTS; + +import android.app.Activity; import android.content.Loader; +import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract.CommonDataKinds.Phone; @@ -34,13 +38,19 @@ import com.android.contacts.common.util.PermissionsUtil; import com.android.contacts.common.util.ViewUtil; import com.android.dialer.R; import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.IntentUtil; +import com.android.dialer.widget.EmptyContentView; +import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; /** * Fragments to show all contacts with phone numbers. */ -public class AllContactsFragment extends ContactEntryListFragment { +public class AllContactsFragment extends ContactEntryListFragment + implements OnEmptyViewActionButtonClickedListener { + + private static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1; - private View mEmptyListView; + private EmptyContentView mEmptyListView; public AllContactsFragment() { setQuickContactEnabled(false); @@ -55,9 +65,10 @@ public class AllContactsFragment extends ContactEntryListFragment= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) { + // Force a refresh of the data since we were missing the permission before this. + reloadData(); + } + } + } } diff --git a/src/com/android/dialer/list/EmptyContactsListAdapter.java b/src/com/android/dialer/list/EmptyContactsListAdapter.java deleted file mode 100644 index 54bd4771f..000000000 --- a/src/com/android/dialer/list/EmptyContactsListAdapter.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.list; - -import android.content.Context; -import android.content.CursorLoader; - -import com.android.contacts.common.list.ContactEntryListAdapter; - -/** - * Used to display an empty contact list when we don't have the permissions to read contacts. - */ -public class EmptyContactsListAdapter extends ContactEntryListAdapter { - - public EmptyContactsListAdapter(Context context) { - super(context); - } - - @Override - public String getContactDisplayName(int position) { - return null; - } - - @Override - public void configureLoader(CursorLoader loader, long directoryId) { - loader.setUri(null); - } - - @Override - public int getCount() { - return 0; - } -} diff --git a/src/com/android/dialer/list/SpeedDialFragment.java b/src/com/android/dialer/list/SpeedDialFragment.java index bf9575858..aead1c81b 100644 --- a/src/com/android/dialer/list/SpeedDialFragment.java +++ b/src/com/android/dialer/list/SpeedDialFragment.java @@ -15,6 +15,8 @@ */ package com.android.dialer.list; +import static android.Manifest.permission.READ_CONTACTS; + import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; @@ -50,6 +52,7 @@ import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; import com.android.contacts.common.util.PermissionsUtil; import com.android.dialer.R; import com.android.dialer.util.DialerUtils; +import com.android.dialer.widget.EmptyContentView; import java.util.ArrayList; import java.util.HashMap; @@ -58,7 +61,8 @@ import java.util.HashMap; * This fragment displays the user's favorite/frequent contacts in a grid. */ public class SpeedDialFragment extends Fragment implements OnItemClickListener, - PhoneFavoritesTileAdapter.OnDataSetChangedForAnimationListener { + PhoneFavoritesTileAdapter.OnDataSetChangedForAnimationListener, + EmptyContentView.OnEmptyViewActionButtonClickedListener { /** * By default, the animation code assumes that all items in a list view are of the same height @@ -81,6 +85,7 @@ public class SpeedDialFragment extends Fragment implements OnItemClickListener, public interface HostInterface { public void setDragDropController(DragDropController controller); + public void showAllContactsTab(); } private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks { @@ -157,7 +162,7 @@ public class SpeedDialFragment extends Fragment implements OnItemClickListener, /** * Layout used when there are no favorites. */ - private View mEmptyView; + private EmptyContentView mEmptyView; private final ContactTileView.Listener mContactTileAdapterListener = new ContactTileAdapterListener(); @@ -197,9 +202,16 @@ public class SpeedDialFragment extends Fragment implements OnItemClickListener, if (getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE) == null) { getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener); + } else { getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE).forceLoad(); } + + mEmptyView.setDescription(R.string.speed_dial_empty); + mEmptyView.setActionLabel(R.string.speed_dial_empty_add_favorite_action); + } else { + mEmptyView.setDescription(R.string.permission_no_speeddial); + mEmptyView.setActionLabel(R.string.permission_single_turn_on); } Trace.endSection(); } @@ -221,9 +233,9 @@ public class SpeedDialFragment extends Fragment implements OnItemClickListener, (ImageView) getActivity().findViewById(R.id.contact_tile_drag_shadow_overlay); mListView.setDragShadowOverlay(dragShadowOverlay); - mEmptyView = mParentView.findViewById(R.id.empty_list_view); - DialerUtils.configureEmptyListView( - mEmptyView, R.drawable.empty_speed_dial, R.string.speed_dial_empty, getResources()); + mEmptyView = (EmptyContentView) mParentView.findViewById(R.id.empty_list_view); + mEmptyView.setImage(R.drawable.empty_speed_dial); + mEmptyView.setActionClickedListener(this); mContactTileFrame = mParentView.findViewById(R.id.contact_tile_frame); @@ -449,4 +461,19 @@ public class SpeedDialFragment extends Fragment implements OnItemClickListener, public AbsListView getListView() { return mListView; } + + @Override + public void onEmptyViewActionButtonClicked(String[] permissions) { + final Activity activity = getActivity(); + if (activity == null) { + return; + } + + if (!PermissionsUtil.hasPermission(activity, READ_CONTACTS)) { + requestPermissions(new String[] {READ_CONTACTS}, 0); + } else { + // Switch tabs + ((HostInterface) activity).showAllContactsTab(); + } + } } diff --git a/src/com/android/dialer/util/DialerUtils.java b/src/com/android/dialer/util/DialerUtils.java index a44c2eec6..e25ada59d 100644 --- a/src/com/android/dialer/util/DialerUtils.java +++ b/src/com/android/dialer/util/DialerUtils.java @@ -40,6 +40,7 @@ import android.widget.Toast; import com.android.contacts.common.ContactsUtils; import com.android.contacts.common.interactions.TouchPointManager; import com.android.dialer.R; +import com.android.dialer.widget.EmptyContentView; import com.android.incallui.CallCardFragment; import com.android.incallui.Log; @@ -115,27 +116,6 @@ public class DialerUtils { return null; } - /** - * Sets the image asset and text for an empty list view (see empty_list_view.xml). - * - * @param emptyListView The empty list view. - * @param imageResId The resource id for the drawable to set as the image. - * @param strResId The resource id for the string to set as the message. - * @param res The resources to obtain the image and string from. - */ - public static void configureEmptyListView( - View emptyListView, int imageResId, int strResId, Resources res) { - ImageView emptyListViewImage = - (ImageView) emptyListView.findViewById(R.id.emptyListViewImage); - - emptyListViewImage.setImageDrawable(res.getDrawable(imageResId)); - emptyListViewImage.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - - TextView emptyListViewMessage = - (TextView) emptyListView.findViewById(R.id.emptyListViewMessage); - emptyListViewMessage.setText(res.getString(strResId)); - } - /** * Closes an {@link AutoCloseable}, silently ignoring any checked exceptions. Does nothing if * null. diff --git a/src/com/android/dialer/widget/EmptyContentView.java b/src/com/android/dialer/widget/EmptyContentView.java new file mode 100644 index 000000000..67c9ce15f --- /dev/null +++ b/src/com/android/dialer/widget/EmptyContentView.java @@ -0,0 +1,102 @@ +/* + * 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.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.dialer.R; + +public class EmptyContentView extends LinearLayout implements View.OnClickListener { + + public static final int NO_LABEL = 0; + + private ImageView mImageView; + private TextView mDescriptionView; + private TextView mActionView; + private String[] mPermissions = new String[] {}; + private OnEmptyViewActionButtonClickedListener mOnActionButtonClickedListener; + + public interface OnEmptyViewActionButtonClickedListener { + public void onEmptyViewActionButtonClicked(String[] permissions); + } + + public EmptyContentView(Context context) { + this(context, null); + } + + public EmptyContentView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public EmptyContentView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public EmptyContentView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setOrientation(LinearLayout.VERTICAL); + + final LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.empty_content_view, this); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mImageView = (ImageView) findViewById(R.id.emptyListViewImage); + mDescriptionView = (TextView) findViewById(R.id.emptyListViewMessage); + mActionView = (TextView) findViewById(R.id.emptyListViewAction); + mActionView.setOnClickListener(this); + } + + public void setDescription(int resourceId) { + mDescriptionView.setText(resourceId); + } + + public void setImage(int resourceId) { + mImageView.setImageResource(resourceId); + } + + public void setActionLabel(int resourceId) { + if (resourceId == NO_LABEL) { + mActionView.setText(null); + mActionView.setVisibility(View.GONE); + } else { + mActionView.setText(resourceId); + mActionView.setVisibility(View.VISIBLE); + } + } + + public void setActionClickedListener(OnEmptyViewActionButtonClickedListener listener) { + mOnActionButtonClickedListener = listener; + } + + @Override + public void onClick(View v) { + if (mOnActionButtonClickedListener != null) { + mOnActionButtonClickedListener.onEmptyViewActionButtonClicked(mPermissions); + } + } +} -- cgit v1.2.3