diff options
author | NathanielWaggoner <nwaggoner@cyngn.com> | 2016-04-19 17:05:06 -0700 |
---|---|---|
committer | Gerrit Code Review <gerrit@cyanogenmod.org> | 2016-04-29 17:59:34 -0700 |
commit | abec0f3ee457d0a60bc5f32940afea343e9ac1c6 (patch) | |
tree | c20b87fc022dd49ad00ac323ac5298483d14bcbe /src | |
parent | 9d76b667dcc76356cde0c7bf8469cf219b4d1671 (diff) | |
download | android_packages_apps_Dialer-abec0f3ee457d0a60bc5f32940afea343e9ac1c6.tar.gz android_packages_apps_Dialer-abec0f3ee457d0a60bc5f32940afea343e9ac1c6.tar.bz2 android_packages_apps_Dialer-abec0f3ee457d0a60bc5f32940afea343e9ac1c6.zip |
Dialer DeepLink Integration
NOTES-53
Change-Id: I90fe73dff9615a9955e61700fe9a712e6a5952ed
Diffstat (limited to 'src')
12 files changed, 830 insertions, 7 deletions
diff --git a/src/com/android/dialer/DialerApplication.java b/src/com/android/dialer/DialerApplication.java index e8e52e481..a5d2e33ec 100644 --- a/src/com/android/dialer/DialerApplication.java +++ b/src/com/android/dialer/DialerApplication.java @@ -25,7 +25,7 @@ import com.android.dialer.discovery.WifiCallStatusNudgeListener; import com.android.dialer.incall.InCallMetricsHelper; import com.android.phone.common.incall.DialerDataSubscription; import com.android.dialer.util.MetricsHelper; - +import com.android.dialer.deeplink.DeepLinkIntegrationManager; public class DialerApplication extends Application { @@ -48,6 +48,7 @@ public class DialerApplication extends Application { MetricsHelper.init(this); WifiCallStatusNudgeListener.init(this); InCallMetricsHelper.init(this); + DeepLinkIntegrationManager.getInstance().setUp(this); Trace.endSection(); } diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java index 7ca61204d..d9e2744b6 100644 --- a/src/com/android/dialer/calllog/CallLogAdapter.java +++ b/src/com/android/dialer/calllog/CallLogAdapter.java @@ -52,10 +52,14 @@ import com.android.dialer.PhoneCallDetails; import com.android.dialer.R; import com.android.dialer.contactinfo.ContactInfoCache; import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener; +import com.android.dialer.deeplink.DeepLinkCache; +import com.android.dialer.deeplink.DeepLinkCache.DeepLinkListener; +import com.android.dialer.deeplink.DeepLinkRequest; import com.android.dialer.util.DialerUtils; import com.android.dialer.util.PhoneNumberUtil; import com.android.dialer.voicemail.VoicemailPlaybackPresenter; +import com.cyanogen.ambient.deeplink.DeepLink; import com.cyanogen.ambient.incall.extension.OriginCodes; import com.google.common.annotations.VisibleForTesting; @@ -98,6 +102,7 @@ public class CallLogAdapter extends GroupingListAdapter private final CallFetcher mCallFetcher; protected ContactInfoCache mContactInfoCache; + protected DeepLinkCache mDeepLinkCache; private boolean mIsShowingRecentsTab; @@ -294,6 +299,13 @@ public class CallLogAdapter extends GroupingListAdapter } }; + protected final DeepLinkListener mDeepLinkListener = new DeepLinkListener() { + @Override + public void onDeepLinkCacheChanged() { + notifyDataSetChanged(); + } + }; + public CallLogAdapter( Context context, CallFetcher callFetcher, @@ -315,6 +327,7 @@ public class CallLogAdapter extends GroupingListAdapter mContactInfoCache = new ContactInfoCache( mContactInfoHelper, mOnContactInfoChangedListener); + mDeepLinkCache = new DeepLinkCache(mDeepLinkListener); if (!PermissionsUtil.hasContactsPermissions(context)) { mContactInfoCache.disableRequestProcessing(); } @@ -371,16 +384,19 @@ public class CallLogAdapter extends GroupingListAdapter public void invalidateCache() { mContactInfoCache.invalidate(); + mDeepLinkCache.invalidate(); } public void startCache() { if (PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) { mContactInfoCache.start(); + mDeepLinkCache.start(); } } public void pauseCache() { mContactInfoCache.stop(); + mDeepLinkCache.stop(); mTelecomCallLogCache.reset(); } @@ -550,7 +566,18 @@ public class CallLogAdapter extends GroupingListAdapter } else { views.inCallComponentName = null; } + views.callTimes = getCallTimes(c, count); + DeepLink dl = mDeepLinkCache.getValue(number, views.callTimes); + if (dl != null && dl != DeepLinkRequest.EMPTY) { + views.mDeepLink = dl; + views.phoneCallDetailsViews.noteIconView.setVisibility(View.VISIBLE); + views.phoneCallDetailsViews.noteIconView.setImageDrawable(dl.getDrawableIcon(mContext)); + } else { + views.mDeepLink = null; + views.phoneCallDetailsViews.noteIconView.setVisibility(View.GONE); + } + views.mDeepLinkPresenter.prepareUi(number); // Check if the day group has changed and display a header if necessary. int currentGroup = getDayGroupForCall(views.rowId); int previousGroup = getPreviousDayGroup(c); @@ -586,6 +613,21 @@ public class CallLogAdapter extends GroupingListAdapter mCallLogListItemHelper.setLookupInfoDetails(views, info); } + /** + * Returns call times for the given number of items in the cursor + */ + private long[] getCallTimes(Cursor cursor, int count) { + int position = cursor.getPosition(); + long[] callTimes = new long[count]; + for (int index = 0; index < count; ++index) { + callTimes[index] = cursor.getLong(CallLogQuery.DATE); + cursor.moveToNext(); + } + cursor.moveToPosition(position); + return callTimes; + } + + @Override public int getItemCount() { return super.getItemCount() + ((isShowingRecentsTab() || mShowPromoCard) ? 1 : 0); diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java index aceaea7c0..b6140c702 100644 --- a/src/com/android/dialer/calllog/CallLogFragment.java +++ b/src/com/android/dialer/calllog/CallLogFragment.java @@ -47,8 +47,12 @@ import com.android.dialer.voicemail.VoicemailPlaybackPresenter; import com.android.dialer.widget.EmptyContentView; import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; import com.android.dialerbind.ObjectFactory; +import com.android.dialer.deeplink.DeepLinkIntegrationManager; import com.android.phone.common.incall.CallMethodInfo; import com.android.phone.common.incall.DialerDataSubscription; +import com.cyanogen.ambient.common.api.ResultCallback; +import com.cyanogen.ambient.deeplink.DeepLink; +import com.cyanogen.ambient.deeplink.applicationtype.DeepLinkApplicationType; import com.cyanogen.ambient.incall.CallLogConstants; import java.util.HashMap; @@ -120,6 +124,10 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis // Exactly same variable is in Fragment as a package private. private boolean mMenuVisible = true; + // track the enabled/disabled status of the DeepLinkApi so we can update the CalLog in onResume + // when returning from settings. + private boolean isDeepLinkApiEnabled = false; + // Default to all calls. private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL; @@ -153,6 +161,19 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis } }; + /* DeepLinkApi global on/off settings check callback. */ + private ResultCallback<DeepLink.BooleanResult> mDeepLinkEnabledCallback = + new ResultCallback<DeepLink.BooleanResult>() { + @Override + public void onResult(DeepLink.BooleanResult result) { + boolean value = result.getResults(); + if (isDeepLinkApiEnabled != value) { + refreshData(); + } + isDeepLinkApiEnabled = value; + } + }; + public interface HostInterface { public void showDialpad(); } @@ -221,7 +242,6 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis } mBlockContactPresenter = new BlockContactPresenter(activity, this); - boolean isShowingRecentsTab = mLogLimit != NO_LOG_LIMIT || mDateLimit != NO_DATE_LIMIT; mAdapter = ObjectFactory.newCallLogAdapter( getActivity(), @@ -230,6 +250,7 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis mVoicemailPlaybackPresenter, mBlockContactPresenter, isShowingRecentsTab); + areDeepLinkEnabled(); } /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ @@ -313,6 +334,15 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); mRecyclerView.setAdapter(mAdapter); + mRecyclerView.setRecyclerListener(new RecyclerView.RecyclerListener() { + @Override + public void onViewRecycled(RecyclerView.ViewHolder holder) { + if (holder instanceof CallLogListItemViewHolder) { + final CallLogListItemViewHolder views = (CallLogListItemViewHolder) holder; + mAdapter.mDeepLinkCache.clearPendingQueries(views.number, views.callTimes); + } + } + }); fetchCalls(); return view; @@ -348,12 +378,13 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis updateEmptyMessage(mCallTypeFilter); } mHasReadCallLogPermission = hasReadCallLogPermission; - if (DialerDataSubscription.get(getActivity()) .subscribe(AMBIENT_SUBSCRIPTION_ID, pluginsUpdatedReceiver)) { refreshData(); mAdapter.startCache(); } + + areDeepLinkEnabled(); } @Override @@ -481,6 +512,11 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis } } + private void areDeepLinkEnabled() { + DeepLinkIntegrationManager.getInstance().isApplicationTypeEnabled( + DeepLinkApplicationType.NOTE, mDeepLinkEnabledCallback); + } + /** * Updates the call data and notification state on entering or leaving the call log tab. * diff --git a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java index c3e445969..d01cb3159 100644 --- a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java +++ b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java @@ -38,6 +38,7 @@ import android.widget.ImageView; import android.widget.TextView; import com.android.dialer.widget.DialerQuickContact; +import com.android.dialer.deeplink.DeepLinkIntegrationManager; import com.android.contacts.common.ContactPhotoManager; import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; @@ -53,6 +54,7 @@ import com.android.dialer.voicemail.VoicemailPlaybackLayout; import com.android.phone.common.incall.CallMethodInfo; import com.android.phone.common.incall.DialerDataSubscription; import com.android.phone.common.incall.utils.CallMethodFilters; +import com.cyanogen.ambient.deeplink.DeepLink; import com.cyanogen.ambient.incall.extension.OriginCodes; /** @@ -81,6 +83,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder public final ImageView primaryActionButtonView; /** DialerQuickContact */ public final DialerQuickContact dialerQuickContact; + public DeepLink mDeepLink; /** The view containing call log item actions. Null until the ViewStub is inflated. */ public View actionsView; @@ -88,6 +91,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder public VoicemailPlaybackLayout voicemailPlaybackView; public View callButtonView; public View videoCallButtonView; + public View viewNoteButton; public View createNewContactButtonView; public View addToExistingContactButtonView; public View sendMessageView; @@ -112,6 +116,12 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder public long[] callIds; /** + * The time for each call in callIds represented by the current call log entry. Used when the + * user views the call log to determine note status + */ + public long[] callTimes; + + /** * The callable phone number for the current call log entry. Cached here as the call back * intent is set only when the actions ViewStub is inflated. */ @@ -186,6 +196,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder private final CallLogListItemHelper mCallLogListItemHelper; private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; private final BlockContactPresenter mBlockContactPresenter; + public final DeepLinkPresenter mDeepLinkPresenter; private final int mPhotoSize; @@ -216,6 +227,8 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder mCallLogListItemHelper = callLogListItemHelper; mVoicemailPlaybackPresenter = voicemailPlaybackPresenter; mBlockContactPresenter = blockContactPresenter; + mDeepLinkPresenter = new DeepLinkPresenter(mContext); + mDeepLinkPresenter.setCallLogViewHolder(this); mContactInfoHelper = contactInfoHelper; this.rootView = rootView; @@ -293,6 +306,9 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder createNewContactButtonView = actionsView.findViewById(R.id.create_new_contact_action); createNewContactButtonView.setOnClickListener(this); + viewNoteButton = actionsView.findViewById(R.id.view_note_action); + viewNoteButton.setOnClickListener(this); + addToExistingContactButtonView = actionsView.findViewById(R.id.add_to_existing_contact_action); addToExistingContactButtonView.setOnClickListener(this); @@ -382,6 +398,12 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder callButtonView.setVisibility(View.GONE); } + if (mDeepLink != null) { + ImageView icon = (ImageView) viewNoteButton.findViewById(R.id.view_note_action_icon); + icon.setImageDrawable(mDeepLink.getDrawableIcon(mContext)); + } else { + viewNoteButton.setVisibility(View.GONE); + } // If one of the calls had video capabilities, show the video call button. if (mTelecomCallLogCache.isVideoEnabled() && canPlaceCallToNumber && phoneCallDetailsViews.callTypeIcons.isVideoShown() || @@ -559,6 +581,10 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder view in dialog. */ numberType, /* phone number type (e.g. mobile) in second line of contact view */ accountHandle); + + } else if (view.getId() == R.id.view_note_action) { + sendOpeningExisitingEvent(); + mContext.startActivity(mDeepLink.createViewIntent()); } else { final String inCallAction = (String) view.getTag(R.id.incall_provider_action_type); if (inCallComponentName != null && !TextUtils.isEmpty(inCallAction)) { @@ -628,4 +654,9 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder return viewHolder; } + private void sendOpeningExisitingEvent() { + DeepLinkIntegrationManager.getInstance().sendContentSentEvent(mContext, mDeepLink, + new ComponentName(mContext, CallLogListItemViewHolder.class)); + } + } diff --git a/src/com/android/dialer/calllog/DeepLinkPresenter.java b/src/com/android/dialer/calllog/DeepLinkPresenter.java new file mode 100644 index 000000000..7e25d3b20 --- /dev/null +++ b/src/com/android/dialer/calllog/DeepLinkPresenter.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2016 The CyanogenMod 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.calllog; + +import android.content.Context; +import android.net.Uri; +import android.view.View; + +import com.cyanogen.ambient.common.api.ResultCallback; +import com.cyanogen.ambient.deeplink.DeepLink; +import com.cyanogen.ambient.deeplink.DeepLink.DeepLinkResultList; +import com.cyanogen.ambient.deeplink.linkcontent.DeepLinkContentType; +import com.cyanogen.ambient.deeplink.applicationtype.DeepLinkApplicationType; + +import com.android.dialer.deeplink.DeepLinkIntegrationManager; + +import java.util.List; +import java.util.ArrayList; + +public class DeepLinkPresenter { + + private CallLogListItemViewHolder mViews; + Context mContext; + + public DeepLinkPresenter(Context context) { + mContext = context; + } + public void setCallLogViewHolder(CallLogListItemViewHolder holder) { + mViews = holder; + } + + public void handleDeepLink(List<DeepLink> links) { + if (links != null) { + for (DeepLink link : links) { + if (link != null && link.getApplicationType() == DeepLinkApplicationType.NOTE + && link.getIcon() != DeepLink.DEFAULT_ICON) { + mViews.mDeepLink = link; + updateViews(); + break; + } + } + } + } + + public void handleReadyForRequests(String number, + ResultCallback<DeepLinkResultList> deepLinkCallback) { + if (mViews.mDeepLink == null) { + List<Uri> uris = buildCallUris(number); + DeepLinkIntegrationManager.getInstance().getPreferredLinksForList(deepLinkCallback, + DeepLinkContentType.CALL, uris); + } else { + updateViews(); + } + } + + private List<Uri> buildCallUris(String number) { + List<Uri> uris = new ArrayList<Uri>(mViews.callTimes.length); + for (int i = 0; i < mViews.callTimes.length; i++) { + uris.add(DeepLinkIntegrationManager.generateCallUri(number, mViews.callTimes[i])); + } + return uris; + } + + private void updateViews() { + mViews.phoneCallDetailsViews.noteIconView.setVisibility(View.VISIBLE); + mViews.phoneCallDetailsViews.noteIconView.setImageDrawable( + mViews.mDeepLink.getDrawableIcon(mContext)); + } + + private final ResultCallback<DeepLinkResultList> deepLinkCallback = new + ResultCallback<DeepLinkResultList>() { + @Override + public void onResult(DeepLinkResultList deepLinkResult) { + handleDeepLink(deepLinkResult.getResults()); + } + }; + + public void prepareUi(final String number) { + handleReadyForRequests(number, deepLinkCallback); + } + +} diff --git a/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java b/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java index df5fe0606..8da402620 100644 --- a/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java +++ b/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java @@ -134,7 +134,9 @@ public class PhoneCallDetailsHelper { } views.nameView.setText(nameText); - + if (views.noteIconView.getDrawable() == null) { + views.noteIconView.setVisibility(View.GONE); + } if (isVoicemail && !TextUtils.isEmpty(details.transcription)) { views.voicemailTranscriptionView.setText(details.transcription); views.voicemailTranscriptionView.setVisibility(View.VISIBLE); diff --git a/src/com/android/dialer/calllog/PhoneCallDetailsViews.java b/src/com/android/dialer/calllog/PhoneCallDetailsViews.java index 94f4411b0..5658b66c1 100644 --- a/src/com/android/dialer/calllog/PhoneCallDetailsViews.java +++ b/src/com/android/dialer/calllog/PhoneCallDetailsViews.java @@ -27,6 +27,8 @@ import com.android.dialer.R; * Encapsulates the views that are used to display the details of a phone call in the call log. */ public final class PhoneCallDetailsViews { + public final View nameWrapper; + public final ImageView noteIconView; public final TextView nameView; public final View callTypeView; public final CallTypeIconsView callTypeIcons; @@ -34,9 +36,11 @@ public final class PhoneCallDetailsViews { public final TextView voicemailTranscriptionView; public final TextView callAccountLabel; - private PhoneCallDetailsViews(TextView nameView, View callTypeView, - CallTypeIconsView callTypeIcons, TextView callLocationAndDate, + private PhoneCallDetailsViews(View nameContainer, ImageView noteIconView, TextView nameView, + View callTypeView, CallTypeIconsView callTypeIcons, TextView callLocationAndDate, TextView voicemailTranscriptionView, TextView callAccountLabel) { + this.nameWrapper = nameContainer; + this.noteIconView = noteIconView; this.nameView = nameView; this.callTypeView = callTypeView; this.callTypeIcons = callTypeIcons; @@ -53,7 +57,10 @@ public final class PhoneCallDetailsViews { * {@code R.id.call_types}. */ public static PhoneCallDetailsViews fromView(View view) { - return new PhoneCallDetailsViews((TextView) view.findViewById(R.id.name), + return new PhoneCallDetailsViews( + view.findViewById(R.id.nameWrapper), + (ImageView) view.findViewById(R.id.hasNotes), + (TextView) view.findViewById(R.id.name), view.findViewById(R.id.call_type), (CallTypeIconsView) view.findViewById(R.id.call_type_icons), (TextView) view.findViewById(R.id.call_location_and_date), @@ -63,6 +70,8 @@ public final class PhoneCallDetailsViews { public static PhoneCallDetailsViews createForTest(Context context) { return new PhoneCallDetailsViews( + new View(context), + new ImageView(context), new TextView(context), new View(context), new CallTypeIconsView(context), diff --git a/src/com/android/dialer/deeplink/DeepLinkCache.java b/src/com/android/dialer/deeplink/DeepLinkCache.java new file mode 100644 index 000000000..068e29c4f --- /dev/null +++ b/src/com/android/dialer/deeplink/DeepLinkCache.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2016 CyanogenMod + * + * 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.deeplink; + +import android.net.Uri; +import android.os.Handler; +import android.os.Message; + +import com.android.dialer.util.ExpirableCache; + +import com.android.dialer.deeplink.DeepLinkIntegrationManager; + +import com.cyanogen.ambient.common.api.PendingResult; +import com.cyanogen.ambient.common.api.ResultCallback; +import com.cyanogen.ambient.deeplink.DeepLink; +import com.cyanogen.ambient.deeplink.applicationtype.DeepLinkApplicationType; +import com.cyanogen.ambient.deeplink.linkcontent.DeepLinkContentType; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +/** + * Cache for managing DeepLinks for the CallLogAdapter and anyone else interested in caching + * DeepLinks. + * + * The methods like getValue(String number, long[] calLTimes) and + * clearPendingQueries(String number, long[] calLTimes) both assume that callTimes are in order + * from mostRecent to eldest, and that this will be the same for a given set of number/call times + * each time. This is used for tracking requests and canceling unnecessary ones. + * + * Expects queries to provide a number and call time. + */ +public class DeepLinkCache { + + /** + * Listener to be notified when the DeepLinkCache has changed. + */ + public interface DeepLinkListener { + /** + * A change has occurred in the DeepLinkCache, and previous state may be invalid. + */ + public void onDeepLinkCacheChanged(); + + } + + private static final int START_THREAD = 0; + private static final int REDRAW = 1; + private static final int DEEP_LINK_CACHE_SIZE = 100; + private static final int START_PROCESSING_REQUESTS_DELAY_MS = 1000; + private static final int PROCESSING_THREAD_THROTTLE_LIMIT = 1000; + private DeepLinkListener mDeepLinkListener; + private final LinkedList<DeepLinkRequest> mRequests; + /** + * Track queries that have bene issued to AmbientCore so we can cancel them if the user leaves + * a context where they are relevant. + */ + private final HashMap<Uri, PendingResult<DeepLink.DeepLinkResultList>> mPendingRequests; + /** + * Cache for DeepLink queries we've already completed. + */ + private ExpirableCache<String, DeepLink> mCache; + + private QueryThread mDeepLinkQueryThread; + private boolean mRequestProcessingDisabled = false; + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case REDRAW: + mDeepLinkListener.onDeepLinkCacheChanged(); + break; + case START_THREAD: + startRequestProcessing(); + break; + } + } + }; + + /** + * Handles requests for DeepLinks as related to a number and call time. + */ + private class QueryThread extends Thread { + private volatile boolean mDone = false; + public volatile boolean needRedraw = false; + + public QueryThread() { + super("DeepLinkCache.QueryThread"); + } + + public void stopProcessing() { + mDone = true; + interrupt(); + } + + @Override + public void run() { + while (true) { + // Check if thread is finished, and if so return immediately. + if (mDone) return; + // Obtain next request, if any is available. + // Keep synchronized section small. + DeepLinkRequest req = null; + synchronized (mRequests) { + if (!mRequests.isEmpty()) { + req = mRequests.removeFirst(); + } + } + if (req != null) { + // Process the request. + queryDeepLinks(req); + } else { + // only update the UI when there are no more requests + if (needRedraw) { + needRedraw = false; + mHandler.sendEmptyMessage(REDRAW); + } + // Wait until another request is available, or until this + // thread is no longer needed (as indicated by being + // interrupted). + try { + synchronized (mRequests) { + mRequests.wait(PROCESSING_THREAD_THROTTLE_LIMIT); + } + } catch (InterruptedException ie) { + // Ignore, and attempt to continue processing requests. + } + } + } + } + } + + public DeepLinkCache(DeepLinkListener listener) { + mRequests = new LinkedList<DeepLinkRequest>(); + mPendingRequests = new HashMap<Uri, PendingResult<DeepLink.DeepLinkResultList>>(); + mCache = ExpirableCache.create(DEEP_LINK_CACHE_SIZE); + mDeepLinkListener = listener; + } + + public DeepLink getValue(String number, long[] times) { + long mostRecentCall = Long.MIN_VALUE; + DeepLink toReturn = null; + List<Uri> urisToRequest = new ArrayList<Uri>(); + boolean immediate = false; + // for all calls + for (long callTime : times) { + // generate the URI + Uri uri = DeepLinkIntegrationManager.generateCallUri(number, callTime); + String uriString = uri.toString(); + // hit the cache, do we have a link for this call? + ExpirableCache.CachedValue<DeepLink> cachedInfo = + mCache.getCachedValue(uriString); + // if so is that a null object? + DeepLink info = cachedInfo == null ? null : cachedInfo.getValue(); + if (cachedInfo == null) { + // if its null we need to add a uri to our requests + mCache.put(uriString, DeepLinkRequest.EMPTY); + urisToRequest.add(uri); + // if we get any uris that haven't been handled we need to immediately do this query + immediate = true; + } else if (cachedInfo.isExpired()) { + urisToRequest.add(uri); + } + if (info != null && info != DeepLinkRequest.EMPTY && callTime > mostRecentCall) { + mostRecentCall = callTime; + toReturn = info; + } + } + // issue new requests for any uri's we haven't handled previously + if (urisToRequest.size() > 0) { + enqueueRequest(urisToRequest, immediate); + } + return toReturn; + } + + + protected void enqueueRequest(List<Uri> uris, boolean immediate) { + DeepLinkRequest request = new DeepLinkRequest(uris); + synchronized (mRequests) { + if (!mRequests.contains(request)) { + mRequests.add(request); + mRequests.notifyAll(); + } + } + if (immediate) { + startRequestProcessing(); + } + } + + + private synchronized void startRequestProcessing() { + if (mRequestProcessingDisabled) return; + + // If a thread is already started, don't start another. + if (mDeepLinkQueryThread != null) { + return; + } + + mDeepLinkQueryThread = new QueryThread(); + mDeepLinkQueryThread.setPriority(Thread.MIN_PRIORITY); + mDeepLinkQueryThread.start(); + } + + /** + * Stops the background thread that processes updates and cancels any + * pending requests to start it. + */ + private synchronized void stopRequestProcessing() { + // Remove any pending requests to start the processing thread. + mHandler.removeMessages(START_THREAD); + if (mDeepLinkQueryThread != null) { + // Stop the thread; we are finished with it. + mDeepLinkQueryThread.stopProcessing(); + mDeepLinkQueryThread = null; + mRequests.clear(); + mPendingRequests.clear(); + } + } + + /** + * Expire the cache in its entirety. + */ + public void invalidate() { + mCache.expireAll(); + stopRequestProcessing(); + } + + /** + * After a delay, start the thread to begin processing requests. We perform lookups on a + * background thread, but this must be called to indicate the thread should be running. + */ + public void start() { + // Schedule a thread-creation message if the thread hasn't been created yet, as an + // optimization to queue fewer messages. + if (mDeepLinkQueryThread == null) { + // TODO: Check whether this delay before starting to process is necessary. + mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MS); + } + } + + /** + * Stops the thread and clears the queue of messages to process. This cleans up the thread + * for lookups so that it is not perpetually running. + */ + public void stop() { + stopRequestProcessing(); + } + + ResultCallback<DeepLink.DeepLinkResultList> mDeepLinkCallback = + new ResultCallback<DeepLink.DeepLinkResultList>() { + @Override + public void onResult(DeepLink.DeepLinkResultList result) { + if (result.getResults() != null) { + handleDeepLinkResults(result.getResults()); + } + } + }; + + private void handleDeepLinkResults(List<DeepLink> results) { + for (DeepLink link : results) { + if (shouldPlaceLinkInCache(link)) { + mCache.put(link.getUri().toString(), link); + if (mDeepLinkQueryThread != null && mPendingRequests.size() <= 0) { + mDeepLinkQueryThread.needRedraw = true; + } + } + } + } + + private boolean shouldPlaceLinkInCache(DeepLink link) { + return link != null && link.getApplicationType() == DeepLinkApplicationType.NOTE && + link.getAlreadyHasContent() && !linkExistsInCache(link); + } + + private boolean linkExistsInCache(DeepLink link) { + DeepLink oldLink = mCache.getPossiblyExpired(link.getUri().toString()); + return link.equals(oldLink); + } + + /** + * Kick off an ambient query for a given request. + * + * @param request - the DeepLinkRequest to query against. + */ + private void queryDeepLinks(DeepLinkRequest request) { + synchronized (mPendingRequests) { + mPendingRequests.put(request.getUris().get(0), + DeepLinkIntegrationManager.getInstance().getPreferredLinksForList( + mDeepLinkCallback, DeepLinkContentType.CALL, request.getUris())); + } + } + + public void clearPendingQueries(String number, long[] calltimes) { + synchronized (mPendingRequests) { + Uri uri = DeepLinkIntegrationManager.generateCallUri(number, calltimes[0]); + if (mPendingRequests.containsKey(uri)) { + PendingResult<DeepLink.DeepLinkResultList> request = mPendingRequests.remove(uri); + if (request!= null) { + request.cancel(); + } + } + } + } +} diff --git a/src/com/android/dialer/deeplink/DeepLinkIntegrationManager.java b/src/com/android/dialer/deeplink/DeepLinkIntegrationManager.java new file mode 100644 index 000000000..8d729a1e9 --- /dev/null +++ b/src/com/android/dialer/deeplink/DeepLinkIntegrationManager.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2016 The CyanogenMod 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.deeplink; + +import android.content.ComponentName; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.provider.CallLog; + +import com.android.phone.common.ambient.AmbientConnection; +import com.cyanogen.ambient.analytics.AnalyticsServices; +import com.cyanogen.ambient.common.ConnectionResult; +import com.cyanogen.ambient.common.CyanogenAmbientUtil; +import com.cyanogen.ambient.common.api.AmbientApiClient; +import com.cyanogen.ambient.common.api.PendingResult; +import com.cyanogen.ambient.common.api.ResultCallback; +import com.cyanogen.ambient.deeplink.DeepLink; +import com.cyanogen.ambient.deeplink.DeepLinkApi; +import com.cyanogen.ambient.deeplink.DeepLinkServices; +import com.cyanogen.ambient.deeplink.applicationtype.DeepLinkApplicationType; +import com.cyanogen.ambient.deeplink.linkcontent.DeepLinkContentType; +import com.cyanogen.ambient.deeplink.metrics.DeepLinkMetricsHelper; +import com.cyanogen.ambient.deeplink.metrics.DeepLinkMetricsHelper.Categories; +import com.cyanogen.ambient.deeplink.metrics.DeepLinkMetricsHelper.Events; +import com.cyanogen.ambient.deeplink.metrics.DeepLinkMetricsHelper.Parameters; + +import java.util.HashMap; +import java.util.List; + +public class DeepLinkIntegrationManager { + + public static DeepLinkIntegrationManager getInstance() { + if (sInstance == null) { + sInstance = new DeepLinkIntegrationManager(); + } + return sInstance; + } + + private String dummyNumber = "00000000"; + private long dummyTime = 0l; + private static DeepLinkIntegrationManager sInstance; + private AmbientApiClient mAmbientApiClient; + private DeepLinkApi mApi; + private volatile boolean mConnected = false; + + public void setUp(Context ctx) { + if(ambientIsAvailable(ctx)) { + mApi = (DeepLinkApi) DeepLinkServices.API; + mAmbientApiClient = AmbientConnection.CLIENT.get(ctx); + } + } + + public PendingResult<DeepLink.DeepLinkResultList> getPreferredLinksFor( + ResultCallback<DeepLink.DeepLinkResultList> callback, DeepLinkContentType category, + Uri uri) { + PendingResult<DeepLink.DeepLinkResultList> result = null; + if (mAmbientApiClient.isConnected()) { + result = mApi.getPreferredLinksForSingleItem(mAmbientApiClient, + DeepLinkApplicationType.NOTE, category, uri); + result.setResultCallback(callback); + } + return result; + } + + public PendingResult<DeepLink.DeepLinkResultList> getPreferredLinksForList( + ResultCallback<DeepLink.DeepLinkResultList> callback, DeepLinkContentType category, + List<Uri> uris) { + PendingResult<DeepLink.DeepLinkResultList> result = null; + if (mAmbientApiClient.isConnected()) { + result = mApi.getPreferredLinksForList(mAmbientApiClient, + DeepLinkApplicationType.NOTE, category, uris); + result.setResultCallback(callback); + } + return result; + } + + public void getDefaultPlugin(ResultCallback<DeepLink.StringResultList> callback, + DeepLinkContentType category) { + PendingResult<DeepLink.StringResultList> result = null; + if (mAmbientApiClient.isConnected()) { + result = mApi.getDefaultProviderDisplayInformation(mAmbientApiClient, + DeepLinkApplicationType.NOTE, category, + DeepLinkIntegrationManager.generateCallUri(dummyNumber, dummyTime)); + result.setResultCallback(callback); + } + } + + /** + * Generate a uri which will identify the call for a given number and timestamp + * + * @param number - the phone number dialed + * @param time - the time the call occured + * @return Uri identifying the call. + */ + public static Uri generateCallUri(String number, long time) { + return Uri.parse(CallLog.AUTHORITY + "." + number + "." + time); + } + + public boolean ambientIsAvailable(Context ctx) { + return CyanogenAmbientUtil.isCyanogenAmbientAvailable(ctx) == CyanogenAmbientUtil.SUCCESS; + } + + public void sendEvent(Context ctx, Categories categories, Events event, + HashMap<Parameters, Object> params) { + if(mAmbientApiClient.isConnected()) { + DeepLinkMetricsHelper.sendEvent(ctx, categories, event, params, mAmbientApiClient); + } + } + + private void sendEvent(Context ctx, DeepLink deepLink, ComponentName cn, + Categories category, Events event) { + + HashMap<Parameters, Object> + parameters = new HashMap<Parameters, Object>(); + + parameters.put(Parameters.SOURCE, cn.flattenToString()); + parameters.put(Parameters.DESTINATION, deepLink.getPackageName()); + parameters.put(Parameters.CONTENT_TYPE, + deepLink.getDeepLinkContentType()); + parameters.put(Parameters.DEST_APPLICATION_TYPE, + deepLink.getApplicationType()); + parameters.put(Parameters.CONTENT_UID, deepLink.getUri().toString()); + sendEvent(ctx, category, event, parameters); + } + + public void sendContentSentEvent(Context ctx, DeepLink deepLink, ComponentName cn) { + sendEvent(ctx, deepLink, cn, Categories.USER_ACTIONS, Events.CONTENT_SENT); + } + + public void sendOpeningExistingEvent(Context ctx, DeepLink deepLink, ComponentName cn) { + sendEvent(ctx, deepLink, cn, Categories.USER_ACTIONS, Events.OPENING_EXISTING_LINK); + } + + + public void openDeepLinkPreferences(DeepLinkApplicationType deepLinkApplicationType) { + if (mAmbientApiClient.isConnected()) { + mApi.openDeepLinkPreferences(mAmbientApiClient, deepLinkApplicationType); + } + } + + public void isApplicationTypeEnabled(DeepLinkApplicationType deepLinkApplicationType, + ResultCallback<DeepLink.BooleanResult> callback) { + if (mAmbientApiClient.isConnected()) { + PendingResult<DeepLink.BooleanResult> result = mApi.isApplicationTypeEnabled( + mAmbientApiClient, deepLinkApplicationType); + result.setResultCallback(callback); + } + } +} diff --git a/src/com/android/dialer/deeplink/DeepLinkListener.java b/src/com/android/dialer/deeplink/DeepLinkListener.java new file mode 100644 index 000000000..65ebec708 --- /dev/null +++ b/src/com/android/dialer/deeplink/DeepLinkListener.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2016 CyanogenMod + * + * 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.deeplink; + +/** + * Listener to be notified when the DeepLinkCache has changed. + */ +public interface DeepLinkListener { + + /** + * A change has occurred in the DeepLinkCache, and previous state may be invalid. + */ + public void onDeepLinkCacheChanged(); + +} diff --git a/src/com/android/dialer/deeplink/DeepLinkRequest.java b/src/com/android/dialer/deeplink/DeepLinkRequest.java new file mode 100644 index 000000000..5d570f2d7 --- /dev/null +++ b/src/com/android/dialer/deeplink/DeepLinkRequest.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2016 CyanogenMod + * + * 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.deeplink; + +import android.net.Uri; +import com.cyanogen.ambient.deeplink.DeepLink; +import java.util.List; + +public class DeepLinkRequest { + public static final DeepLink EMPTY = new DeepLink(null, null, null, null, -1, -1, null, null, + false); + /** + * The uris for this set of number and times + */ + private List<Uri> mUris; + + public DeepLinkRequest(List<Uri> uris) { + mUris = uris; + } + + public List<Uri> getUris() { + return mUris; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof DeepLinkRequest)) return false; + + DeepLinkRequest other = (DeepLinkRequest) obj; + return mUris.equals(other.mUris); + } + + @Override + public int hashCode() { + return mUris.hashCode(); + } +} diff --git a/src/com/android/dialer/settings/DialerSettingsActivity.java b/src/com/android/dialer/settings/DialerSettingsActivity.java index 27fa53416..31df4b238 100644 --- a/src/com/android/dialer/settings/DialerSettingsActivity.java +++ b/src/com/android/dialer/settings/DialerSettingsActivity.java @@ -11,6 +11,8 @@ import android.preference.PreferenceScreen; import android.util.Log; import android.widget.CompoundButton; import android.widget.Switch; + +import com.android.dialer.deeplink.DeepLinkIntegrationManager; import com.android.internal.annotations.VisibleForTesting; import com.android.phone.common.ambient.AmbientConnection; import com.android.phone.common.incall.DialerDataSubscription; @@ -21,6 +23,9 @@ import com.cyanogen.ambient.callerinfo.util.CallerInfoHelper; import com.cyanogen.ambient.callerinfo.util.ProviderInfo; import com.cyanogen.ambient.common.api.PendingResult; import com.cyanogen.ambient.common.api.ResultCallback; +import com.cyanogen.ambient.deeplink.DeepLink; +import com.cyanogen.ambient.deeplink.applicationtype.DeepLinkApplicationType; +import com.cyanogen.ambient.deeplink.linkcontent.DeepLinkContentType; import com.cyanogen.ambient.plugin.PluginStatus; import com.cyanogen.ambient.incall.util.InCallHelper; import com.google.common.collect.Lists; @@ -72,6 +77,7 @@ public class DialerSettingsActivity extends PreferenceActivity { PreferenceScreen mPreferenceScreen; List<CallMethodInfo> mCallProviders = new ArrayList<>(); List<Header> mCurrentHeaders = Lists.newArrayList(); + List<String> mDeepLinkPluginInfo; private static final String AMBIENT_SUBSCRIPTION_ID = "DialerSettingsActivity"; @@ -83,6 +89,22 @@ public class DialerSettingsActivity extends PreferenceActivity { } }; + private ResultCallback<DeepLink.StringResultList> mDeepLinkCallback = + new ResultCallback<DeepLink.StringResultList>() { + @Override + public void onResult(DeepLink.StringResultList result) { + List<String> results = result.getResults(); + if (results != null && results.size() > 0) { + deepLinkUpdated(results); + } + } + }; + + private void deepLinkUpdated(List<String> deepLinkPluginInfo) { + mDeepLinkPluginInfo = deepLinkPluginInfo; + invalidateHeaders(); + } + private void providersUpdated(HashMap<ComponentName, CallMethodInfo> callMethodInfos) { mCallProviders.clear(); mCallProviders.addAll(callMethodInfos.values()); @@ -106,6 +128,9 @@ public class DialerSettingsActivity extends PreferenceActivity { CallerInfoHelper.getProviderInfo(this, mSelectedProvider.getComponent()); } } + DeepLinkIntegrationManager.getInstance().getDefaultPlugin(mDeepLinkCallback, + DeepLinkContentType.CALL); + super.onCreate(savedInstanceState); mPreferences = PreferenceManager.getDefaultSharedPreferences(this); } @@ -224,6 +249,13 @@ public class DialerSettingsActivity extends PreferenceActivity { } } + if (mDeepLinkPluginInfo != null) { + Header noteHeader = new Header(); + noteHeader.title = mDeepLinkPluginInfo.get(0); + noteHeader.summaryRes = R.string.note_mod_settings_summary; + target.add(noteHeader); + } + // invalidateHeaders does not rebuild // the list properly, so if an adapter is present already // then we've invalidated and need make sure we notify the list. @@ -571,4 +603,13 @@ public class DialerSettingsActivity extends PreferenceActivity { asyncTask.execute(status); return status; } + + @Override + public void onHeaderClick(Header header, int position) { + super.onHeaderClick(header, position); + if (header.summaryRes == R.string.note_mod_settings_summary) { + DeepLinkIntegrationManager.getInstance().openDeepLinkPreferences + (DeepLinkApplicationType.NOTE); + } + } } |