1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.globalsearch; 18 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.content.pm.PackageManager; 22 import android.content.pm.ProviderInfo; 23 import android.util.Log; 24 import android.view.KeyEvent; 25 26 import java.util.List; 27 28 /** 29 * Logs clicks to an external content provider. 30 * The click log provider is protected by a permission to avoid log stuffing, 31 * and the click log provider must have a special permission to avoid log stealing. 32 */ 33 public class ClickLogger { 34 35 private static final boolean DBG = false; 36 private static final String TAG = "GlobalSearch.ClickLogger"; 37 38 private final Context mContext; 39 40 private final ProviderInfo mLogReceiverInfo; 41 ClickLogger(Context context, ProviderInfo logReceiverInfo)42 private ClickLogger(Context context, ProviderInfo logReceiverInfo) { 43 mContext = context; 44 mLogReceiverInfo = logReceiverInfo; 45 } 46 47 /** 48 * Logs a click. 49 * 50 * @param query Query as typed by the user. 51 * @param clickPos The index of the clicked suggestion. 52 * @param displayedSuggestions The suggestions that were displayed. 53 * @param actionKey action key used to click the suggestion, or KeyEvent.KEYCODE_UNKNOWN. 54 * @param actionMsg action message for the action key used, or {@code null}. 55 */ logClick(String query, int clickPos, List<SuggestionData> displayedSuggestions, int actionKey, String actionMsg)56 public void logClick(String query, int clickPos, List<SuggestionData> displayedSuggestions, 57 int actionKey, String actionMsg) { 58 int suggestionCount = displayedSuggestions.size(); 59 if (clickPos < 0 || clickPos >= suggestionCount) { 60 Log.w(TAG, "Click out of range: " + clickPos + ", count: " + suggestionCount); 61 return; 62 } 63 SuggestionData clicked = displayedSuggestions.get(clickPos); 64 // Only log clicks on suggestions that come from the same package as the log receiver. 65 if (!isSuggestionFromLogReceiver(clicked)) { 66 return; 67 } 68 ContentValues row = new ContentValues(); 69 row.put(ClickLoggerContract.COL_QUERY, query); 70 row.put(ClickLoggerContract.COL_POS, clickPos); 71 String extraData = clicked.getIntentExtraData(); 72 if (extraData != null) { 73 row.put(ClickLoggerContract.COL_EXTRA_DATA, extraData); 74 } 75 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { 76 row.put(ClickLoggerContract.COL_ACTION_KEY, actionKey); 77 } 78 if (actionMsg != null) { 79 row.put(ClickLoggerContract.COL_ACTION_MSG, actionMsg); 80 } 81 StringBuilder slots = new StringBuilder(); 82 for (int i = 0; i < suggestionCount; i++) { 83 SuggestionData suggestion = displayedSuggestions.get(i); 84 slots.append(getSlotInfo(suggestion)); 85 if (i < suggestionCount - 1) { 86 slots.append(","); 87 } 88 } 89 row.put(ClickLoggerContract.COL_SLOTS, slots.toString()); 90 try { 91 if (DBG) Log.d(TAG, "insert(" + ClickLoggerContract.CLICK_LOG_URI + "," + row + ")"); 92 mContext.getContentResolver().insert(ClickLoggerContract.CLICK_LOG_URI, row); 93 } catch (RuntimeException ex) { 94 // Guard against buggy logger implementations 95 Log.e(TAG, "Failed to log click: " + ex); 96 } 97 } 98 99 /** 100 * Checks whether the given suggestion comes from the same application as the one 101 * hosting the click log receiver. 102 */ isSuggestionFromLogReceiver(SuggestionData clicked)103 private boolean isSuggestionFromLogReceiver(SuggestionData clicked) { 104 String packageName = clicked.getSource().getPackageName(); 105 String receiverPackage = mLogReceiverInfo.applicationInfo.packageName; 106 return packageName != null && packageName.equals(receiverPackage); 107 } 108 109 /** 110 * Gets the information that we will log for each displayed suggestion. 111 */ getSlotInfo(SuggestionData suggestion)112 private String getSlotInfo(SuggestionData suggestion) { 113 if (SuggestionFactoryImpl.BUILTIN_SOURCE_COMPONENT.equals(suggestion.getSource())) { 114 return ClickLoggerContract.TYPE_BUILTIN; 115 } else if (isSuggestionFromLogReceiver(suggestion)) { 116 // This is a bit of a hack, we assume that the app that handles log requests is 117 // the web suggestion source. 118 return ClickLoggerContract.TYPE_WEB; 119 } else { 120 return ClickLoggerContract.TYPE_OTHER; 121 } 122 } 123 124 /** 125 * Gets a click logger, if clicks should be logged. 126 * 127 * @param context 128 * @return A click logger, or {@code null} if clicks should not be logged. 129 */ getClickLogger(Context context)130 public static ClickLogger getClickLogger(Context context) { 131 PackageManager pm = context.getPackageManager(); 132 ProviderInfo providerInfo = 133 pm.resolveContentProvider(ClickLoggerContract.LOG_AUHTORITY, 0); 134 if (providerInfo == null) { 135 // This is not an error, since the platform may not include a click logger 136 if (DBG) Log.d(TAG, "No provider found for " + ClickLoggerContract.LOG_AUHTORITY); 137 return null; 138 } 139 String providerPackage = providerInfo.applicationInfo.packageName; 140 // Check if the content provider has permission to receive click log data 141 if (pm.checkPermission(ClickLoggerContract.PERMISSION_RECEIVE_GLOBALSEARCH_LOG, 142 providerPackage) != PackageManager.PERMISSION_GRANTED) { 143 Log.w(TAG, "Package " + providerPackage + " does not have permission " 144 + ClickLoggerContract.PERMISSION_RECEIVE_GLOBALSEARCH_LOG); 145 return null; 146 } 147 return new ClickLogger(context, providerInfo); 148 } 149 150 } 151