diff --git a/api/cm_current.txt b/api/cm_current.txt index 1d0f4f7..daf5fea 100644 --- a/api/cm_current.txt +++ b/api/cm_current.txt @@ -463,6 +463,7 @@ package cyanogenmod.platform { public static final class Manifest.permission { ctor public Manifest.permission(); + field public static final java.lang.String ACCESS_APP_SUGGESTIONS = "cyanogenmod.permission.ACCESS_APP_SUGGESTIONS"; field public static final java.lang.String HARDWARE_ABSTRACTION_ACCESS = "cyanogenmod.permission.HARDWARE_ABSTRACTION_ACCESS"; field public static final java.lang.String MANAGE_ALARMS = "cyanogenmod.permission.MANAGE_ALARMS"; field public static final java.lang.String MANAGE_PERSISTENT_STORAGE = "cyanogenmod.permission.MANAGE_PERSISTENT_STORAGE"; @@ -482,10 +483,18 @@ package cyanogenmod.platform { ctor public R(); } + public static final class R.array { + ctor public R.array(); + } + public static final class R.attr { ctor public R.attr(); } + public static final class R.bool { + ctor public R.bool(); + } + public static final class R.drawable { ctor public R.drawable(); } diff --git a/cm/lib/main/java/org/cyanogenmod/platform/internal/AppSuggestManagerService.java b/cm/lib/main/java/org/cyanogenmod/platform/internal/AppSuggestManagerService.java new file mode 100644 index 0000000..d7a6ad4 --- /dev/null +++ b/cm/lib/main/java/org/cyanogenmod/platform/internal/AppSuggestManagerService.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015, 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 org.cyanogenmod.platform.internal; + +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; +import android.util.Slog; +import com.android.server.SystemService; + +import cyanogenmod.app.CMContextConstants; +import cyanogenmod.app.suggest.ApplicationSuggestion; +import cyanogenmod.app.suggest.IAppSuggestManager; +import cyanogenmod.platform.Manifest; + +import java.util.ArrayList; +import java.util.List; + +public class AppSuggestManagerService extends SystemService { + private static final String TAG = "AppSgstMgrService"; + public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + public static final String NAME = "appsuggest"; + + public static final String ACTION = "org.cyanogenmod.app.suggest"; + + private AppSuggestProviderInterface mImpl; + + private final IBinder mService = new IAppSuggestManager.Stub() { + public boolean handles(Intent intent) { + if (mImpl == null) return false; + + return mImpl.handles(intent); + } + + public List getSuggestions(Intent intent) { + if (mImpl == null) return new ArrayList<>(0); + + return mImpl.getSuggestions(intent); + } + }; + + public AppSuggestManagerService(Context context) { + super(context); + } + + @Override + public void onStart() { + mImpl = AppSuggestProviderProxy.createAndBind(mContext, TAG, ACTION, + R.bool.config_enableAppSuggestOverlay, + R.string.config_appSuggestProviderPackageName, + R.array.config_appSuggestProviderPackageNames); + if (mImpl == null) { + Slog.e(TAG, "no app suggest provider found"); + } else { + Slog.i(TAG, "Bound to to suggest provider"); + } + + publishBinderService(CMContextConstants.CM_APP_SUGGEST_SERVICE, mService); + } +} diff --git a/cm/lib/main/java/org/cyanogenmod/platform/internal/AppSuggestProviderInterface.java b/cm/lib/main/java/org/cyanogenmod/platform/internal/AppSuggestProviderInterface.java new file mode 100644 index 0000000..da815ce --- /dev/null +++ b/cm/lib/main/java/org/cyanogenmod/platform/internal/AppSuggestProviderInterface.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015, 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 org.cyanogenmod.platform.internal; + +import android.content.Intent; +import cyanogenmod.app.suggest.ApplicationSuggestion; + +import java.util.List; + +/** + * App Suggestion Manager's interface for Applicaiton Suggestion Providers. + * + * @hide + */ +public interface AppSuggestProviderInterface { + boolean handles(Intent intent); + List getSuggestions(Intent intent); +} diff --git a/cm/lib/main/java/org/cyanogenmod/platform/internal/AppSuggestProviderProxy.java b/cm/lib/main/java/org/cyanogenmod/platform/internal/AppSuggestProviderProxy.java new file mode 100644 index 0000000..0357f73 --- /dev/null +++ b/cm/lib/main/java/org/cyanogenmod/platform/internal/AppSuggestProviderProxy.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2015, 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 org.cyanogenmod.platform.internal; + +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.RemoteException; +import android.util.Log; +import com.android.server.ServiceWatcher; + +import cyanogenmod.app.suggest.ApplicationSuggestion; +import cyanogenmod.app.suggest.IAppSuggestProvider; + +import java.util.ArrayList; +import java.util.List; + +/** + * @hide + */ +public class AppSuggestProviderProxy implements AppSuggestProviderInterface { + private static final String TAG = AppSuggestProviderProxy.class.getSimpleName(); + private static final boolean DEBUG = AppSuggestManagerService.DEBUG; + + public static AppSuggestProviderProxy createAndBind( + Context context, String name, String action, + int overlaySwitchResId, int defaultServicePackageNameResId, + int initialPackageNamesResId) { + AppSuggestProviderProxy proxy = new AppSuggestProviderProxy(context, name, action, + overlaySwitchResId, defaultServicePackageNameResId, initialPackageNamesResId); + if (proxy.bind()) { + return proxy; + } else { + return null; + } + } + + private Context mContext; + private ServiceWatcher mServiceWatcher; + + private AppSuggestProviderProxy(Context context, String name, String action, + int overlaySwitchResId, int defaultServicePackageNameResId, + int initialPackageNamesResId) { + mContext = context; + mServiceWatcher = new ServiceWatcher(mContext, TAG + "-" + name, action, overlaySwitchResId, + defaultServicePackageNameResId, initialPackageNamesResId, null, null); + } + + private boolean bind() { + return mServiceWatcher.start(); + } + + private IAppSuggestProvider getService() { + return IAppSuggestProvider.Stub.asInterface(mServiceWatcher.getBinder()); + } + + @Override + public boolean handles(Intent intent) { + IAppSuggestProvider service = getService(); + if (service == null) return false; + + try { + return service.handles(intent); + } catch (RemoteException e) { + Log.w(TAG, e); + } catch (Exception e) { + // never let remote service crash system server + Log.e(TAG, "Exception from " + mServiceWatcher.getBestPackageName(), e); + } + return false; + } + + @Override + public List getSuggestions(Intent intent) { + IAppSuggestProvider service = getService(); + if (service == null) return new ArrayList<>(0); + + try { + return service.getSuggestions(intent); + } catch (RemoteException e) { + Log.w(TAG, e); + } catch (Exception e) { + // never let remote service crash system server + Log.e(TAG, "Exception from " + mServiceWatcher.getBestPackageName(), e); + } + return new ArrayList<>(0); + } +} diff --git a/cm/res/AndroidManifest.xml b/cm/res/AndroidManifest.xml index 8c40827..ec801cd 100644 --- a/cm/res/AndroidManifest.xml +++ b/cm/res/AndroidManifest.xml @@ -120,6 +120,13 @@ android:description="@string/permdesc_managePersistentStorage" android:protectionLevel="system|signature" /> + + + + + + + true + + + com.cyanogen.app.suggest + + + + com.cyanogen.app.suggest + + \ No newline at end of file diff --git a/cm/res/res/values/strings.xml b/cm/res/res/values/strings.xml index e727080..bcfed5a 100644 --- a/cm/res/res/values/strings.xml +++ b/cm/res/res/values/strings.xml @@ -70,6 +70,10 @@ manage persistent storage Allows an app to read or write properties which may persist thrοugh a factory reset. + + access application suggestions + Allows an app to access application suggestions. + Custom tile listener diff --git a/cm/res/res/values/symbols.xml b/cm/res/res/values/symbols.xml index 3dcf497..7977939 100644 --- a/cm/res/res/values/symbols.xml +++ b/cm/res/res/values/symbols.xml @@ -19,6 +19,10 @@ SDK. Instead, put them here. --> + + + + diff --git a/packages/CMResolver/Android.mk b/packages/CMResolver/Android.mk new file mode 100644 index 0000000..b11027c --- /dev/null +++ b/packages/CMResolver/Android.mk @@ -0,0 +1,36 @@ +# +# Copyright (C) 2015 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. +# +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +src_dir := src +res_dir := res + +LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dir)) +LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dir)) + +LOCAL_PACKAGE_NAME := CMResolver +LOCAL_CERTIFICATE := platform +LOCAL_PRIVILEGED_MODULE := true + +LOCAL_STATIC_JAVA_LIBRARIES := \ + org.cyanogenmod.platform.sdk + +include $(BUILD_PACKAGE) + +######################## +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/packages/CMResolver/AndroidManifest.xml b/packages/CMResolver/AndroidManifest.xml new file mode 100644 index 0000000..47ce4bf --- /dev/null +++ b/packages/CMResolver/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + diff --git a/packages/CMResolver/res/drawable-hdpi/play_download.png b/packages/CMResolver/res/drawable-hdpi/play_download.png new file mode 100644 index 0000000..a0e280c Binary files /dev/null and b/packages/CMResolver/res/drawable-hdpi/play_download.png differ diff --git a/packages/CMResolver/res/drawable-mdpi/play_download.png b/packages/CMResolver/res/drawable-mdpi/play_download.png new file mode 100644 index 0000000..e9ccbaa Binary files /dev/null and b/packages/CMResolver/res/drawable-mdpi/play_download.png differ diff --git a/packages/CMResolver/res/drawable-xhdpi/play_download.png b/packages/CMResolver/res/drawable-xhdpi/play_download.png new file mode 100644 index 0000000..bd2ccc3 Binary files /dev/null and b/packages/CMResolver/res/drawable-xhdpi/play_download.png differ diff --git a/packages/CMResolver/res/drawable-xxhdpi/play_download.png b/packages/CMResolver/res/drawable-xxhdpi/play_download.png new file mode 100644 index 0000000..5f6d062 Binary files /dev/null and b/packages/CMResolver/res/drawable-xxhdpi/play_download.png differ diff --git a/packages/CMResolver/res/drawable-xxxhdpi/play_download.png b/packages/CMResolver/res/drawable-xxxhdpi/play_download.png new file mode 100644 index 0000000..81ac1de Binary files /dev/null and b/packages/CMResolver/res/drawable-xxxhdpi/play_download.png differ diff --git a/packages/CMResolver/res/drawable/icon.png b/packages/CMResolver/res/drawable/icon.png new file mode 100644 index 0000000..08ee50d Binary files /dev/null and b/packages/CMResolver/res/drawable/icon.png differ diff --git a/packages/CMResolver/res/layout/suggest_list_item.xml b/packages/CMResolver/res/layout/suggest_list_item.xml new file mode 100644 index 0000000..1747203 --- /dev/null +++ b/packages/CMResolver/res/layout/suggest_list_item.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + diff --git a/packages/CMResolver/res/values/strings.xml b/packages/CMResolver/res/values/strings.xml new file mode 100644 index 0000000..7f4f705 --- /dev/null +++ b/packages/CMResolver/res/values/strings.xml @@ -0,0 +1,21 @@ + + + + + CyanogenMod Resolver + Download and open with + \ No newline at end of file diff --git a/packages/CMResolver/res/values/themes.xml b/packages/CMResolver/res/values/themes.xml new file mode 100644 index 0000000..c138bea --- /dev/null +++ b/packages/CMResolver/res/values/themes.xml @@ -0,0 +1,32 @@ + + + + + + \ No newline at end of file diff --git a/packages/CMResolver/src/org/cyanogenmod/resolver/ResolverActivity.java b/packages/CMResolver/src/org/cyanogenmod/resolver/ResolverActivity.java new file mode 100644 index 0000000..8c9c1e3 --- /dev/null +++ b/packages/CMResolver/src/org/cyanogenmod/resolver/ResolverActivity.java @@ -0,0 +1,1447 @@ +/* + * Copyright (C) 2008 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 org.cyanogenmod.resolver; + +import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityManagerNative; +import android.app.ActivityThread; +import android.app.AppGlobals; +import android.app.usage.UsageStats; +import android.app.usage.UsageStatsManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.LabeledIntent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.UserInfo; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.PatternMatcher; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; +import android.util.Slog; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; +import com.android.internal.R; +import com.android.internal.content.PackageMonitor; +import com.android.internal.widget.ResolverDrawerLayout; +import cyanogenmod.app.suggest.AppSuggestManager; +import cyanogenmod.app.suggest.ApplicationSuggestion; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; +import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; + +/** + * This activity is displayed when the system attempts to start an Intent for + * which there is more than one matching activity, allowing the user to decide + * which to go to. It is not normally used directly by application developers. + */ +public class ResolverActivity extends Activity implements AdapterView.OnItemClickListener { + private static final String TAG = "ResolverActivity"; + private static final boolean DEBUG = false; + + private int mLaunchedFromUid; + private ResolveListAdapter mAdapter; + private ApplicationSuggestionAdapter mSuggestAdapter; + private AppSuggestManager mSuggest; + private PackageManager mPm; + private boolean mSafeForwardingMode; + private boolean mAlwaysUseOption; + private boolean mShowExtended; + private ListView mListView; + private boolean mHasSuggestions; + private ViewGroup mFilteredItemContainer; + private Button mAlwaysButton; + private Button mOnceButton; + private View mProfileView; + private int mIconDpi; + private int mIconSize; + private int mMaxColumns; + private int mLastSelected = ListView.INVALID_POSITION; + private boolean mResolvingHome = false; + private int mProfileSwitchMessageId = -1; + private Intent mIntent; + + private boolean mUsingSuggestions; + + private UsageStatsManager mUsm; + private Map mStats; + private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 14; + + private boolean mRegistered; + private final PackageMonitor mPackageMonitor = new PackageMonitor() { + @Override public void onSomePackagesChanged() { + mAdapter.handlePackagesChanged(); + mSuggestAdapter.handlePackagesChanged(); + if (mAdapter.getCount() == 0 && !mHasSuggestions) { + // We no longer have any items... just finish the activity. + finish(); + } else { + ListAdapter d = mListView.getAdapter(); + if (mHasSuggestions) { + if (d != mSuggestAdapter) { + mListView.setAdapter(mSuggestAdapter); + } + } else if (d != mAdapter) { + mListView.setAdapter(mAdapter); + } else { + // keep using the same adapter + } + } + if (mProfileView != null) { + bindProfileView(); + } + } + }; + + private enum ActionTitle { + VIEW(Intent.ACTION_VIEW, + R.string.whichViewApplication, + R.string.whichViewApplicationNamed), + EDIT(Intent.ACTION_EDIT, + R.string.whichEditApplication, + R.string.whichEditApplicationNamed), + SEND(Intent.ACTION_SEND, + R.string.whichSendApplication, + R.string.whichSendApplicationNamed), + SENDTO(Intent.ACTION_SENDTO, + R.string.whichSendApplication, + R.string.whichSendApplicationNamed), + SEND_MULTIPLE(Intent.ACTION_SEND_MULTIPLE, + R.string.whichSendApplication, + R.string.whichSendApplicationNamed), + DEFAULT(null, + R.string.whichApplication, + R.string.whichApplicationNamed), + HOME(Intent.ACTION_MAIN, + R.string.whichHomeApplication, + R.string.whichHomeApplicationNamed); + + public final String action; + public final int titleRes; + public final int namedTitleRes; + + ActionTitle(String action, int titleRes, int namedTitleRes) { + this.action = action; + this.titleRes = titleRes; + this.namedTitleRes = namedTitleRes; + } + + public static ActionTitle forAction(String action) { + for (ActionTitle title : values()) { + if (title != HOME && action != null && action.equals(title.action)) { + return title; + } + } + return DEFAULT; + } + } + + private Intent makeMyIntent() { + Intent intent = new Intent(getIntent()); + intent.setComponent(null); + // The resolver activity is set to be hidden from recent tasks. + // we don't want this attribute to be propagated to the next activity + // being launched. Note that if the original Intent also had this + // flag set, we are now losing it. That should be a very rare case + // and we can live with this. + intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + return intent; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + // Use a specialized prompt when we're handling the 'Home' app startActivity() + final Intent intent = makeMyIntent(); + final Set categories = intent.getCategories(); + if (Intent.ACTION_MAIN.equals(intent.getAction()) + && categories != null + && categories.size() == 1 + && categories.contains(Intent.CATEGORY_HOME)) { + // Note: this field is not set to true in the compatibility version. + mResolvingHome = true; + } + + setSafeForwardingMode(true); + + onCreate(savedInstanceState, intent, null, 0, null, null, true); + } + + /** + * Compatibility version for other bundled services that use this ocerload without + * a default title resource + */ + protected void onCreate(Bundle savedInstanceState, Intent intent, + CharSequence title, Intent[] initialIntents, + List rList, boolean alwaysUseOption) { + onCreate(savedInstanceState, intent, title, 0, initialIntents, rList, alwaysUseOption); + } + + protected void onCreate(Bundle savedInstanceState, Intent intent, + CharSequence title, int defaultTitleRes, Intent[] initialIntents, + List rList, boolean alwaysUseOption) { + super.onCreate(savedInstanceState); + + mSuggest = AppSuggestManager.getInstance(this); + // Determine whether we should show that intent is forwarded + // from managed profile to owner or other way around. + setProfileSwitchMessageId(intent.getContentUserHint()); + + try { + mLaunchedFromUid = ActivityManagerNative.getDefault().getLaunchedFromUid( + getActivityToken()); + } catch (RemoteException e) { + mLaunchedFromUid = -1; + } + mPm = getPackageManager(); + mUsm = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); + + final long sinceTime = System.currentTimeMillis() - USAGE_STATS_PERIOD; + mStats = mUsm.queryAndAggregateUsageStats(sinceTime, System.currentTimeMillis()); + + mMaxColumns = getResources().getInteger(R.integer.config_maxResolverActivityColumns); + + mPackageMonitor.register(this, getMainLooper(), false); + mRegistered = true; + + final ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE); + mIconDpi = am.getLauncherLargeIconDensity(); + mIconSize = am.getLauncherLargeIconSize(); + + mIntent = new Intent(intent); + mAdapter = new ResolveListAdapter(this, initialIntents, rList, + mLaunchedFromUid, alwaysUseOption); + + mSuggestAdapter = new ApplicationSuggestionAdapter(this); + + final int layoutId; + final boolean useHeader; + if (mAdapter.hasFilteredItem()) { + layoutId = R.layout.resolver_list_with_default; + alwaysUseOption = false; + useHeader = true; + } else { + useHeader = false; + layoutId = R.layout.resolver_list; + } + mAlwaysUseOption = alwaysUseOption; + + if (mLaunchedFromUid < 0 || UserHandle.isIsolated(mLaunchedFromUid)) { + // Gulp! + finish(); + return; + } + + mHasSuggestions = mSuggest.handles(mIntent); + + int count = mAdapter.mList.size(); + if (count > 1 || (count == 1 && mAdapter.getOtherProfile() != null)) { + setContentView(layoutId); + mListView = (ListView) findViewById(R.id.resolver_list); + mListView.setAdapter(mAdapter); + mListView.setOnItemClickListener(this); + mListView.setOnItemLongClickListener(new ItemLongClickListener()); + + if (alwaysUseOption) { + mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + } + + if (useHeader) { + mListView.addHeaderView(LayoutInflater.from(this).inflate( + R.layout.resolver_different_item_header, mListView, false)); + } + + mUsingSuggestions = false; + } else if (count == 1 && !mHasSuggestions) { + safelyStartActivity(mAdapter.intentForPosition(0, false)); + mPackageMonitor.unregister(); + mRegistered = false; + finish(); + return; + } else { + setContentView(R.layout.resolver_list); + + mListView = (ListView) findViewById(R.id.resolver_list); + + if (!mHasSuggestions) { + final TextView empty = (TextView) findViewById(R.id.empty); + empty.setVisibility(View.VISIBLE); + mListView.setVisibility(View.GONE); + mUsingSuggestions = false; + } else { + mListView.setVisibility(View.VISIBLE); + mListView.setAdapter(mSuggestAdapter); + mListView.setOnItemClickListener(this); + mUsingSuggestions = true; + } + } + // Prevent the Resolver window from becoming the top fullscreen window and thus from taking + // control of the system bars. + getWindow().clearFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR); + + final ResolverDrawerLayout rdl = (ResolverDrawerLayout) findViewById(R.id.contentPanel); + if (rdl != null) { + rdl.setOnDismissedListener(new ResolverDrawerLayout.OnDismissedListener() { + @Override + public void onDismissed() { + finish(); + } + }); + } + + if (title == null) { + if (!mUsingSuggestions) { + title = getTitleForAction(intent.getAction(), defaultTitleRes); + } else { + title = getString(org.cyanogenmod.resolver.R.string.download_and_open_with); + } + } + if (!TextUtils.isEmpty(title)) { + final TextView titleView = (TextView) findViewById(R.id.title); + if (titleView != null) { + titleView.setText(title); + } + setTitle(title); + } + + final ImageView iconView = (ImageView) findViewById(R.id.icon); + final DisplayResolveInfo iconInfo = mAdapter.getFilteredItem(); + if (iconView != null && iconInfo != null) { + new LoadIconIntoViewTask(iconView).execute(iconInfo); + } + + if (alwaysUseOption || mAdapter.hasFilteredItem()) { + final ViewGroup buttonLayout = (ViewGroup) findViewById(R.id.button_bar); + if (buttonLayout != null && !mUsingSuggestions) { + buttonLayout.setVisibility(View.VISIBLE); + mAlwaysButton = (Button) buttonLayout.findViewById(R.id.button_always); + mOnceButton = (Button) buttonLayout.findViewById(R.id.button_once); + mAlwaysButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onButtonClick(v); + } + }); + mOnceButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onButtonClick(v); + } + }); + } else { + mAlwaysUseOption = false; + } + } + + if (mAdapter.hasFilteredItem()) { + mFilteredItemContainer = (ViewGroup) findViewById(R.id.filtered_item_container); + mFilteredItemContainer.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + DisplayResolveInfo filteredItem = mAdapter.getFilteredItem(); + + if (filteredItem == null) { + return false; + } + + showAppDetails(filteredItem.ri); + return true; + } + }); + + setAlwaysButtonEnabled(true, mAdapter.getFilteredPosition(), false); + mOnceButton.setEnabled(true); + } + + mProfileView = findViewById(R.id.profile_button); + if (mProfileView != null) { + mProfileView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + final DisplayResolveInfo dri = mAdapter.getOtherProfile(); + if (dri == null) { + return; + } + + final Intent intent = intentForDisplayResolveInfo(dri); + onIntentSelected(dri.ri, intent, false); + finish(); + } + }); + bindProfileView(); + } + } + + void bindProfileView() { + final DisplayResolveInfo dri = mAdapter.getOtherProfile(); + if (dri != null) { + mProfileView.setVisibility(View.VISIBLE); + final ImageView icon = (ImageView) mProfileView.findViewById(R.id.icon); + final TextView text = (TextView) mProfileView.findViewById(R.id.text1); + if (dri.displayIcon == null) { + new LoadIconTask().execute(dri); + } + icon.setImageDrawable(dri.displayIcon); + text.setText(dri.displayLabel); + } else { + mProfileView.setVisibility(View.GONE); + } + } + + private void setProfileSwitchMessageId(int contentUserHint) { + if (contentUserHint != UserHandle.USER_CURRENT && + contentUserHint != UserHandle.myUserId()) { + UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); + UserInfo originUserInfo = userManager.getUserInfo(contentUserHint); + boolean originIsManaged = originUserInfo != null ? originUserInfo.isManagedProfile() + : false; + boolean targetIsManaged = userManager.isManagedProfile(); + if (originIsManaged && !targetIsManaged) { + mProfileSwitchMessageId = R.string.forward_intent_to_owner; + } else if (!originIsManaged && targetIsManaged) { + mProfileSwitchMessageId = R.string.forward_intent_to_work; + } + } + } + + /** + * Turn on launch mode that is safe to use when forwarding intents received from + * applications and running in system processes. This mode uses Activity.startActivityAsCaller + * instead of the normal Activity.startActivity for launching the activity selected + * by the user. + * + *

This mode is set to true by default if the activity is initialized through + * {@link #onCreate(Bundle)}. If a subclass calls one of the other onCreate + * methods, it is set to false by default. You must set it before calling one of the + * more detailed onCreate methods, so that it will be set correctly in the case where + * there is only one intent to resolve and it is thus started immediately.

+ */ + public void setSafeForwardingMode(boolean safeForwarding) { + mSafeForwardingMode = safeForwarding; + } + + protected CharSequence getTitleForAction(String action, int defaultTitleRes) { + final ActionTitle title = mResolvingHome ? ActionTitle.HOME : ActionTitle.forAction(action); + final boolean named = mAdapter.hasFilteredItem(); + if (title == ActionTitle.DEFAULT && defaultTitleRes != 0) { + return getString(defaultTitleRes); + } else { + return named ? getString(title.namedTitleRes, mAdapter.getFilteredItem().displayLabel) : + getString(title.titleRes); + } + } + + void dismiss() { + if (!isFinishing()) { + finish(); + } + } + + Drawable getIcon(Resources res, int resId) { + Drawable result; + try { + result = res.getDrawableForDensity(resId, mIconDpi); + } catch (Resources.NotFoundException e) { + result = null; + } + + return result; + } + + Drawable loadIconForResolveInfo(ResolveInfo ri) { + Drawable dr; + try { + if (ri.resolvePackageName != null && ri.icon != 0) { + dr = getIcon(mPm.getResourcesForApplication(ri.resolvePackageName), ri.icon); + if (dr != null) { + return dr; + } + } + final int iconRes = ri.getIconResource(); + if (iconRes != 0) { + dr = getIcon(mPm.getResourcesForApplication(ri.activityInfo.packageName), iconRes); + if (dr != null) { + return dr; + } + } + } catch (NameNotFoundException e) { + Log.e(TAG, "Couldn't find resources for package", e); + } + return ri.loadIcon(mPm); + } + + @Override + protected void onRestart() { + super.onRestart(); + if (!mRegistered) { + mPackageMonitor.register(this, getMainLooper(), false); + mRegistered = true; + } + mAdapter.handlePackagesChanged(); + mSuggestAdapter.handlePackagesChanged(); + if (mProfileView != null) { + bindProfileView(); + } + } + + @Override + protected void onStop() { + super.onStop(); + if (mRegistered) { + mPackageMonitor.unregister(); + mRegistered = false; + } + if ((getIntent().getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) != 0) { + // This resolver is in the unusual situation where it has been + // launched at the top of a new task. We don't let it be added + // to the recent tasks shown to the user, and we need to make sure + // that each time we are launched we get the correct launching + // uid (not re-using the same resolver from an old launching uid), + // so we will now finish ourself since being no longer visible, + // the user probably can't get back to us. + if (!isChangingConfigurations()) { + finish(); + } + } + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + if (mAlwaysUseOption) { + final int checkedPos = mListView.getCheckedItemPosition(); + final boolean hasValidSelection = checkedPos != ListView.INVALID_POSITION; + mLastSelected = checkedPos; + setAlwaysButtonEnabled(hasValidSelection, checkedPos, true); + mOnceButton.setEnabled(hasValidSelection); + if (hasValidSelection) { + mListView.setSelection(checkedPos); + } + } + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + position -= mListView.getHeaderViewsCount(); + if (position < 0) { + // Header views don't count. + return; + } + ListAdapter d = mListView.getAdapter(); + if (d == mAdapter) { + ResolveInfo resolveInfo = mAdapter.resolveInfoForPosition(position, true); + if (mResolvingHome && hasManagedProfile() + && !supportsManagedProfiles(resolveInfo)) { + Toast.makeText(this, String.format(getResources().getString( + R.string.activity_resolver_work_profiles_support), + resolveInfo.activityInfo.loadLabel(getPackageManager()).toString()), + Toast.LENGTH_LONG).show(); + return; + } + final int checkedPos = mListView.getCheckedItemPosition(); + final boolean hasValidSelection = checkedPos != ListView.INVALID_POSITION; + if (mAlwaysUseOption && (!hasValidSelection || mLastSelected != checkedPos)) { + setAlwaysButtonEnabled(hasValidSelection, checkedPos, true); + mOnceButton.setEnabled(hasValidSelection); + if (hasValidSelection) { + mListView.smoothScrollToPosition(checkedPos); + } + mLastSelected = checkedPos; + } else { + startSelected(position, false, true); + } + } else { + showMarket(mSuggestAdapter.getItem(position).suggestion); + } + } + + private void showMarket(ApplicationSuggestion item) { + Intent in = new Intent().setAction(Intent.ACTION_VIEW) + .setData(item.getDownloadUri()) + .addFlags((Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET)); + startActivity(in); + } + + private boolean hasManagedProfile() { + UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); + if (userManager == null) { + return false; + } + + try { + List profiles = userManager.getProfiles(getUserId()); + for (UserInfo userInfo : profiles) { + if (userInfo != null && userInfo.isManagedProfile()) { + return true; + } + } + } catch (SecurityException e) { + return false; + } + return false; + } + + private boolean supportsManagedProfiles(ResolveInfo resolveInfo) { + try { + ApplicationInfo appInfo = getPackageManager().getApplicationInfo( + resolveInfo.activityInfo.packageName, 0 /* default flags */); + return versionNumberAtLeastL(appInfo.targetSdkVersion); + } catch (NameNotFoundException e) { + return false; + } + } + + private boolean versionNumberAtLeastL(int versionNumber) { + return versionNumber >= Build.VERSION_CODES.LOLLIPOP; + } + + private void setAlwaysButtonEnabled(boolean hasValidSelection, int checkedPos, + boolean filtered) { + boolean enabled = false; + if (hasValidSelection) { + ResolveInfo ri = mAdapter.resolveInfoForPosition(checkedPos, filtered); + if (ri.targetUserId == UserHandle.USER_CURRENT) { + enabled = true; + } + } + mAlwaysButton.setEnabled(enabled); + } + + public void onButtonClick(View v) { + final int id = v.getId(); + switch(id) { + case R.id.button_always: + case R.id.button_once: + case R.id.filtered_item_container: { + startSelected(mAlwaysUseOption ? + mListView.getCheckedItemPosition() : mAdapter.getFilteredPosition(), + id == R.id.button_always, + mAlwaysUseOption); + dismiss(); + break; + } case org.cyanogenmod.resolver.R.id.suggest_item_container: { + DisplayApplicationSuggestion s = mSuggestAdapter.getRecommended(); + if (s != null) { + showMarket(s.suggestion); + } + break; + } + } + } + + void startSelected(int which, boolean always, boolean filtered) { + if (isFinishing()) { + return; + } + ResolveInfo ri = mAdapter.resolveInfoForPosition(which, filtered); + Intent intent = mAdapter.intentForPosition(which, filtered); + onIntentSelected(ri, intent, always); + finish(); + } + + /** + * Replace me in subclasses! + */ + public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { + return defIntent; + } + + protected void onIntentSelected(ResolveInfo ri, Intent intent, boolean alwaysCheck) { + if ((mAlwaysUseOption || mAdapter.hasFilteredItem()) && mAdapter.mOrigResolveList != null) { + // Build a reasonable intent filter, based on what matched. + IntentFilter filter = new IntentFilter(); + + if (intent.getAction() != null) { + filter.addAction(intent.getAction()); + } + Set categories = intent.getCategories(); + if (categories != null) { + for (String cat : categories) { + filter.addCategory(cat); + } + } + filter.addCategory(Intent.CATEGORY_DEFAULT); + + int cat = ri.match&IntentFilter.MATCH_CATEGORY_MASK; + Uri data = intent.getData(); + if (cat == IntentFilter.MATCH_CATEGORY_TYPE) { + String mimeType = intent.resolveType(this); + if (mimeType != null) { + try { + filter.addDataType(mimeType); + } catch (IntentFilter.MalformedMimeTypeException e) { + Log.w("ResolverActivity", e); + filter = null; + } + } + } + if (data != null && data.getScheme() != null) { + // We need the data specification if there was no type, + // OR if the scheme is not one of our magical "file:" + // or "content:" schemes (see IntentFilter for the reason). + if (cat != IntentFilter.MATCH_CATEGORY_TYPE + || (!"file".equals(data.getScheme()) + && !"content".equals(data.getScheme()))) { + filter.addDataScheme(data.getScheme()); + + // Look through the resolved filter to determine which part + // of it matched the original Intent. + Iterator pIt = ri.filter.schemeSpecificPartsIterator(); + if (pIt != null) { + String ssp = data.getSchemeSpecificPart(); + while (ssp != null && pIt.hasNext()) { + PatternMatcher p = pIt.next(); + if (p.match(ssp)) { + filter.addDataSchemeSpecificPart(p.getPath(), p.getType()); + break; + } + } + } + Iterator aIt = ri.filter.authoritiesIterator(); + if (aIt != null) { + while (aIt.hasNext()) { + IntentFilter.AuthorityEntry a = aIt.next(); + if (a.match(data) >= 0) { + int port = a.getPort(); + filter.addDataAuthority(a.getHost(), + port >= 0 ? Integer.toString(port) : null); + break; + } + } + } + pIt = ri.filter.pathsIterator(); + if (pIt != null) { + String path = data.getPath(); + while (path != null && pIt.hasNext()) { + PatternMatcher p = pIt.next(); + if (p.match(path)) { + filter.addDataPath(p.getPath(), p.getType()); + break; + } + } + } + } + } + + if (filter != null) { + final int N = mAdapter.mOrigResolveList.size(); + ComponentName[] set = new ComponentName[N]; + int bestMatch = 0; + for (int i=0; i bestMatch) bestMatch = r.match; + } + if (alwaysCheck) { + getPackageManager().addPreferredActivity(filter, bestMatch, set, + intent.getComponent()); + } else { + try { + AppGlobals.getPackageManager().setLastChosenActivity(intent, + intent.resolveTypeIfNeeded(getContentResolver()), + PackageManager.MATCH_DEFAULT_ONLY, + filter, bestMatch, intent.getComponent()); + } catch (RemoteException re) { + Log.d(TAG, "Error calling setLastChosenActivity\n" + re); + } + } + } + } + + if (intent != null) { + safelyStartActivity(intent); + } + } + + public void safelyStartActivity(Intent intent) { + // If needed, show that intent is forwarded + // from managed profile to owner or other way around. + if (mProfileSwitchMessageId != -1) { + Toast.makeText(this, getString(mProfileSwitchMessageId), Toast.LENGTH_LONG).show(); + } + if (!mSafeForwardingMode) { + startActivity(intent); + onActivityStarted(intent); + return; + } + try { + startActivityAsCaller(intent, null, UserHandle.USER_NULL); + onActivityStarted(intent); + } catch (RuntimeException e) { + String launchedFromPackage; + try { + launchedFromPackage = ActivityManagerNative.getDefault().getLaunchedFromPackage( + getActivityToken()); + } catch (RemoteException e2) { + launchedFromPackage = "??"; + } + Slog.wtf(TAG, "Unable to launch as uid " + mLaunchedFromUid + + " package " + launchedFromPackage + ", while running in " + + ActivityThread.currentProcessName(), e); + } + } + + public void onActivityStarted(Intent intent) { + // Do nothing + } + + void showAppDetails(ResolveInfo ri) { + Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", ri.activityInfo.packageName, null)) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + startActivity(in); + } + + Intent intentForDisplayResolveInfo(DisplayResolveInfo dri) { + Intent intent = new Intent(dri.origIntent != null ? dri.origIntent : + getReplacementIntent(dri.ri.activityInfo, mIntent)); + intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT + |Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); + ActivityInfo ai = dri.ri.activityInfo; + intent.setComponent(new ComponentName( + ai.applicationInfo.packageName, ai.name)); + return intent; + } + + private final class DisplayResolveInfo { + ResolveInfo ri; + CharSequence displayLabel; + Drawable displayIcon; + CharSequence extendedInfo; + Intent origIntent; + + DisplayResolveInfo(ResolveInfo pri, CharSequence pLabel, + CharSequence pInfo, Intent pOrigIntent) { + ri = pri; + displayLabel = pLabel; + extendedInfo = pInfo; + origIntent = pOrigIntent; + } + } + + private final class ResolveListAdapter extends BaseAdapter { + private final Intent[] mInitialIntents; + private final List mBaseResolveList; + private ResolveInfo mLastChosen; + private DisplayResolveInfo mOtherProfile; + private final int mLaunchedFromUid; + private final LayoutInflater mInflater; + + List mList; + List mOrigResolveList; + + private int mLastChosenPosition = -1; + private boolean mFilterLastUsed; + + public ResolveListAdapter(Context context, Intent[] initialIntents, + List rList, int launchedFromUid, boolean filterLastUsed) { + mInitialIntents = initialIntents; + mBaseResolveList = rList; + mLaunchedFromUid = launchedFromUid; + mInflater = LayoutInflater.from(context); + mList = new ArrayList(); + mFilterLastUsed = filterLastUsed; + rebuildList(); + } + + public void handlePackagesChanged() { + rebuildList(); + notifyDataSetChanged(); + } + + public DisplayResolveInfo getFilteredItem() { + if (mFilterLastUsed && mLastChosenPosition >= 0) { + // Not using getItem since it offsets to dodge this position for the list + return mList.get(mLastChosenPosition); + } + return null; + } + + public DisplayResolveInfo getOtherProfile() { + return mOtherProfile; + } + + public int getFilteredPosition() { + if (mFilterLastUsed && mLastChosenPosition >= 0) { + return mLastChosenPosition; + } + return AbsListView.INVALID_POSITION; + } + + public boolean hasFilteredItem() { + return mFilterLastUsed && mLastChosenPosition >= 0; + } + + private void rebuildList() { + List currentResolveList; + + try { + mLastChosen = AppGlobals.getPackageManager().getLastChosenActivity( + mIntent, mIntent.resolveTypeIfNeeded(getContentResolver()), + PackageManager.MATCH_DEFAULT_ONLY); + } catch (RemoteException re) { + Log.d(TAG, "Error calling setLastChosenActivity\n" + re); + } + + mList.clear(); + if (mBaseResolveList != null) { + currentResolveList = mOrigResolveList = mBaseResolveList; + } else { + currentResolveList = mOrigResolveList = mPm.queryIntentActivities( + mIntent, PackageManager.MATCH_DEFAULT_ONLY + | (mFilterLastUsed ? PackageManager.GET_RESOLVED_FILTER : 0)); + // Filter out any activities that the launched uid does not + // have permission for. We don't do this when we have an explicit + // list of resolved activities, because that only happens when + // we are being subclassed, so we can safely launch whatever + // they gave us. + if (currentResolveList != null) { + for (int i=currentResolveList.size()-1; i >= 0; i--) { + String thisName = ResolverActivity.class.getCanonicalName(); + if (!currentResolveList.get(i).activityInfo.name.equals(thisName)) { + ActivityInfo ai = currentResolveList.get(i).activityInfo; + int granted = ActivityManager.checkComponentPermission( + ai.permission, mLaunchedFromUid, + ai.applicationInfo.uid, ai.exported); + if (granted != PackageManager.PERMISSION_GRANTED) { + // Access not allowed! + if (mOrigResolveList == currentResolveList) { + mOrigResolveList = new ArrayList(mOrigResolveList); + } + currentResolveList.remove(i); + } + } else { + currentResolveList.remove(i); + } + } + } + } + int N; + if ((currentResolveList != null) && ((N = currentResolveList.size()) > 0)) { + // Only display the first matches that are either of equal + // priority or have asked to be default options. + ResolveInfo r0 = currentResolveList.get(0); + for (int i=1; i(mOrigResolveList); + } + currentResolveList.remove(i); + N--; + } + } + } + if (N > 1) { + Comparator rComparator = + new ResolverComparator(ResolverActivity.this, mIntent); + Collections.sort(currentResolveList, rComparator); + } + // First put the initial items at the top. + if (mInitialIntents != null) { + for (int i=0; i= 0) { + mLastChosenPosition = -1; + mFilterLastUsed = false; + } + } + + private void processGroup(List rList, int start, int end, ResolveInfo ro, + CharSequence roLabel) { + // Process labels from start to i + int num = end - start+1; + if (num == 1) { + // No duplicate labels. Use label for entry at start + addResolveInfo(new DisplayResolveInfo(ro, roLabel, null, null)); + updateLastChosenPosition(ro); + } else { + mShowExtended = true; + boolean usePkg = false; + CharSequence startApp = ro.activityInfo.applicationInfo.loadLabel(mPm); + if (startApp == null) { + usePkg = true; + } + if (!usePkg) { + // Use HashSet to track duplicates + HashSet duplicates = + new HashSet(); + duplicates.add(startApp); + for (int j = start+1; j <= end ; j++) { + ResolveInfo jRi = rList.get(j); + CharSequence jApp = jRi.activityInfo.applicationInfo.loadLabel(mPm); + if ( (jApp == null) || (duplicates.contains(jApp))) { + usePkg = true; + break; + } else { + duplicates.add(jApp); + } + } + // Clear HashSet for later use + duplicates.clear(); + } + for (int k = start; k <= end; k++) { + ResolveInfo add = rList.get(k); + if (usePkg) { + // Use application name for all entries from start to end-1 + addResolveInfo(new DisplayResolveInfo(add, roLabel, + add.activityInfo.packageName, null)); + } else { + // Use package name for all entries from start to end-1 + addResolveInfo(new DisplayResolveInfo(add, roLabel, + add.activityInfo.applicationInfo.loadLabel(mPm), null)); + } + updateLastChosenPosition(add); + } + } + } + + private void updateLastChosenPosition(ResolveInfo info) { + if (mLastChosen != null + && mLastChosen.activityInfo.packageName.equals(info.activityInfo.packageName) + && mLastChosen.activityInfo.name.equals(info.activityInfo.name)) { + mLastChosenPosition = mList.size() - 1; + } + } + + private void addResolveInfo(DisplayResolveInfo dri) { + if (dri.ri.targetUserId != UserHandle.USER_CURRENT && mOtherProfile == null) { + // So far we only support a single other profile at a time. + // The first one we see gets special treatment. + mOtherProfile = dri; + } else { + mList.add(dri); + } + } + + public ResolveInfo resolveInfoForPosition(int position, boolean filtered) { + return (filtered ? getItem(position) : mList.get(position)).ri; + } + + public Intent intentForPosition(int position, boolean filtered) { + DisplayResolveInfo dri = filtered ? getItem(position) : mList.get(position); + return intentForDisplayResolveInfo(dri); + } + + public int getCount() { + int result = mList.size(); + if (mFilterLastUsed && mLastChosenPosition >= 0) { + result--; + } + return result; + } + + public DisplayResolveInfo getItem(int position) { + if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) { + position++; + } + return mList.get(position); + } + + public long getItemId(int position) { + return position; + } + + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + view = mInflater.inflate( + R.layout.resolve_list_item, parent, false); + + final ViewHolder holder = new ViewHolder(view); + view.setTag(holder); + } + bindView(view, getItem(position)); + return view; + } + + private final void bindView(View view, DisplayResolveInfo info) { + final ViewHolder holder = (ViewHolder) view.getTag(); + holder.text.setText(info.displayLabel); + if (mShowExtended) { + holder.text2.setVisibility(View.VISIBLE); + holder.text2.setText(info.extendedInfo); + } else { + holder.text2.setVisibility(View.GONE); + } + if (info.displayIcon == null) { + new LoadIconTask().execute(info); + } + holder.icon.setImageDrawable(info.displayIcon); + } + } + + static class ViewHolder { + public TextView text; + public TextView text2; + public ImageView icon; + + public ViewHolder(View view) { + text = (TextView) view.findViewById(R.id.text1); + text2 = (TextView) view.findViewById(R.id.text2); + icon = (ImageView) view.findViewById(R.id.icon); + } + } + + class ItemLongClickListener implements AdapterView.OnItemLongClickListener { + + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + position -= mListView.getHeaderViewsCount(); + if (position < 0) { + // Header views don't count. + return false; + } + ListAdapter d = mListView.getAdapter(); + if (d == mAdapter) { + ResolveInfo ri = mAdapter.resolveInfoForPosition(position, true); + showAppDetails(ri); + } else { + // Suggestions don't support long click, so skip + } + return true; + } + + } + + class LoadIconTask extends AsyncTask { + @Override + protected DisplayResolveInfo doInBackground(DisplayResolveInfo... params) { + final DisplayResolveInfo info = params[0]; + if (info.displayIcon == null) { + info.displayIcon = loadIconForResolveInfo(info.ri); + } + return info; + } + + @Override + protected void onPostExecute(DisplayResolveInfo info) { + if (mProfileView != null && mAdapter.getOtherProfile() == info) { + bindProfileView(); + } + mAdapter.notifyDataSetChanged(); + } + } + + class LoadIconIntoViewTask extends AsyncTask { + final ImageView mTargetView; + + public LoadIconIntoViewTask(ImageView target) { + mTargetView = target; + } + + @Override + protected DisplayResolveInfo doInBackground(DisplayResolveInfo... params) { + final DisplayResolveInfo info = params[0]; + if (info.displayIcon == null) { + info.displayIcon = loadIconForResolveInfo(info.ri); + } + return info; + } + + @Override + protected void onPostExecute(DisplayResolveInfo info) { + mTargetView.setImageDrawable(info.displayIcon); + } + } + + private final class DisplayApplicationSuggestion { + ApplicationSuggestion suggestion; + Drawable displayIcon; + + public DisplayApplicationSuggestion(ApplicationSuggestion suggestion, Drawable icon) { + this.suggestion = suggestion; + this.displayIcon = icon; + } + } + + private final class ApplicationSuggestionAdapter extends BaseAdapter { + private LayoutInflater mInflater; + + public List mList; + + public ApplicationSuggestionAdapter(Context context) { + mInflater = LayoutInflater.from(context); + mList = new ArrayList<>(); + handlePackagesChanged(); + } + + @Override + public int getCount() { + return mList.size(); + } + + public DisplayApplicationSuggestion getItem(int position) { + return mList.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + view = mInflater.inflate( + org.cyanogenmod.resolver.R.layout.suggest_list_item, parent, false); + + final SuggestViewHolder holder = new SuggestViewHolder(view); + view.setTag(holder); + } + bindView(view, getItem(position)); + return view; + } + + public void bindView(View view, DisplayApplicationSuggestion item) { + SuggestViewHolder holder = (SuggestViewHolder)view.getTag(); + holder.name.setText(item.suggestion.getName()); + if (item.displayIcon == null) { + new LoadSuggestIconTask().execute(item); + } else { + holder.icon.setImageDrawable(item.displayIcon); + } + + holder.icon2.setVisibility( + "com.android.vending".equals(item.suggestion.getPackageName()) ? + View.GONE : View.VISIBLE + ); + } + + public void handlePackagesChanged() { + new AsyncTask>() { + @Override + public void onPreExecute() { + + } + + @Override + public List doInBackground(Void ... args) { + return mSuggest.getSuggestions(mIntent); + } + + @Override + public void onPostExecute(List result) { + mList.clear(); + for (ApplicationSuggestion s : result) { + mList.add(new DisplayApplicationSuggestion(s, null)); + } + notifyDataSetChanged(); + } + }.execute(); + + + } + + public DisplayApplicationSuggestion getRecommended() { + return !mList.isEmpty() ? mList.get(0) : null; + } + } + + static class SuggestViewHolder { + TextView name; + ImageView icon; + ImageView icon2; + + public SuggestViewHolder(View view) { + name = (TextView)view.findViewById(R.id.text1); + icon = (ImageView)view.findViewById(R.id.icon); + icon2 = (ImageView)view.findViewById(R.id.icon2); + } + } + + class LoadSuggestIconTask extends AsyncTask { + @Override + protected DisplayApplicationSuggestion doInBackground(DisplayApplicationSuggestion... params) { + params[0].displayIcon = mSuggest.loadIcon(params[0].suggestion); + return params[0]; + } + + @Override + protected void onPostExecute(DisplayApplicationSuggestion result) { + if (result.displayIcon != null) { + mSuggestAdapter.notifyDataSetChanged(); + } + } + } + + static final boolean isSpecificUriMatch(int match) { + match = match&IntentFilter.MATCH_CATEGORY_MASK; + return match >= IntentFilter.MATCH_CATEGORY_HOST + && match <= IntentFilter.MATCH_CATEGORY_PATH; + } + + class ResolverComparator implements Comparator { + private final Collator mCollator; + private final boolean mHttp; + + public ResolverComparator(Context context, Intent intent) { + mCollator = Collator.getInstance(context.getResources().getConfiguration().locale); + String scheme = intent.getScheme(); + mHttp = "http".equals(scheme) || "https".equals(scheme); + } + + @Override + public int compare(ResolveInfo lhs, ResolveInfo rhs) { + // We want to put the one targeted to another user at the end of the dialog. + if (lhs.targetUserId != UserHandle.USER_CURRENT) { + return 1; + } + + if (mHttp) { + // Special case: we want filters that match URI paths/schemes to be + // ordered before others. This is for the case when opening URIs, + // to make native apps go above browsers. + final boolean lhsSpecific = isSpecificUriMatch(lhs.match); + final boolean rhsSpecific = isSpecificUriMatch(rhs.match); + if (lhsSpecific != rhsSpecific) { + return lhsSpecific ? -1 : 1; + } + } + + if (mStats != null) { + final long timeDiff = + getPackageTimeSpent(rhs.activityInfo.packageName) - + getPackageTimeSpent(lhs.activityInfo.packageName); + + if (timeDiff != 0) { + return timeDiff > 0 ? 1 : -1; + } + } + + CharSequence sa = lhs.loadLabel(mPm); + if (sa == null) sa = lhs.activityInfo.name; + CharSequence sb = rhs.loadLabel(mPm); + if (sb == null) sb = rhs.activityInfo.name; + + return mCollator.compare(sa.toString(), sb.toString()); + } + + private long getPackageTimeSpent(String packageName) { + if (mStats != null) { + final UsageStats stats = mStats.get(packageName); + if (stats != null) { + return stats.getTotalTimeInForeground(); + } + + } + return 0; + } + } +} diff --git a/src/java/cyanogenmod/app/CMContextConstants.java b/src/java/cyanogenmod/app/CMContextConstants.java index ab80b4f..b2278b1 100644 --- a/src/java/cyanogenmod/app/CMContextConstants.java +++ b/src/java/cyanogenmod/app/CMContextConstants.java @@ -85,4 +85,9 @@ public final class CMContextConstants { * @hide */ public static final String CM_HARDWARE_SERVICE = "cmhardware"; + + /** + * @hide + */ + public static final String CM_APP_SUGGEST_SERVICE = "cmappsuggest"; } diff --git a/src/java/cyanogenmod/app/suggest/AppSuggestManager.java b/src/java/cyanogenmod/app/suggest/AppSuggestManager.java new file mode 100644 index 0000000..7bc034c --- /dev/null +++ b/src/java/cyanogenmod/app/suggest/AppSuggestManager.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2015, 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 cyanogenmod.app.suggest; + +import android.content.Context; +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import cyanogenmod.app.CMContextConstants; +import cyanogenmod.app.suggest.ApplicationSuggestion; + +/** + * Provides an interface to get information about suggested apps for an intent which may include + * applications not installed on the device. This is used by the CMResolver in order to provide + * suggestions when an intent is fired but no application exists for the given intent. + * + * @hide + */ +public class AppSuggestManager { + private static final String TAG = AppSuggestManager.class.getSimpleName(); + private static final boolean DEBUG = true; + + private static IAppSuggestManager sImpl; + + private static AppSuggestManager sInstance; + + private Context mContext; + + /** + * Gets an instance of the AppSuggestManager. + * + * @param context + * + * @return An instance of the AppSuggestManager + */ + public static synchronized AppSuggestManager getInstance(Context context) { + if (sInstance != null) { + return sInstance; + } + + context = context.getApplicationContext() != null ? context.getApplicationContext() : context; + + sInstance = new AppSuggestManager(context); + + return sInstance; + } + + private AppSuggestManager(Context context) { + mContext = context.getApplicationContext(); + } + + private static synchronized IAppSuggestManager getService() { + if (sImpl == null) { + IBinder b = ServiceManager.getService(CMContextConstants.CM_APP_SUGGEST_SERVICE); + if (b != null) { + sImpl = IAppSuggestManager.Stub.asInterface(b); + } else { + Log.e(TAG, "Unable to find implementation for app suggest service"); + } + } + + return sImpl; + } + + /** + * Checks to see if an intent is handled by the App Suggestions Service. This should be + * implemented in such a way that it is safe to call inline on the UI Thread. + * + * @param intent The intent + * @return true if the App Suggestions Service has suggestions for this intent, false otherwise + */ + public boolean handles(Intent intent) { + IAppSuggestManager mgr = getService(); + if (mgr == null) return false; + try { + return mgr.handles(intent); + } catch (RemoteException e) { + return false; + } + } + + /** + * + * Gets a list of the suggestions for the given intent. + * + * @param intent The intent + * @return A list of application suggestions or an empty list if none. + */ + public List getSuggestions(Intent intent) { + IAppSuggestManager mgr = getService(); + if (mgr == null) return new ArrayList<>(0); + try { + return mgr.getSuggestions(intent); + } catch (RemoteException e) { + return new ArrayList<>(0); + } + } + + /** + * Loads the icon for the given suggestion. + * + * @param suggestion The suggestion to load the icon for + * + * @return A {@link Drawable} or null if one cannot be found + */ + public Drawable loadIcon(ApplicationSuggestion suggestion) { + try { + InputStream is = mContext.getContentResolver() + .openInputStream(suggestion.getThumbailUri()); + return Drawable.createFromStream(is, null); + } catch (FileNotFoundException e) { + return null; + } + } +} diff --git a/src/java/cyanogenmod/app/suggest/ApplicationSuggestion.aidl b/src/java/cyanogenmod/app/suggest/ApplicationSuggestion.aidl new file mode 100644 index 0000000..7ab8584 --- /dev/null +++ b/src/java/cyanogenmod/app/suggest/ApplicationSuggestion.aidl @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015, 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 cyanogenmod.app.suggest; + +/** + * @hide + */ +parcelable ApplicationSuggestion; diff --git a/src/java/cyanogenmod/app/suggest/ApplicationSuggestion.java b/src/java/cyanogenmod/app/suggest/ApplicationSuggestion.java new file mode 100644 index 0000000..c10afe3 --- /dev/null +++ b/src/java/cyanogenmod/app/suggest/ApplicationSuggestion.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2015, 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 cyanogenmod.app.suggest; + +import android.annotation.NonNull; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import cyanogenmod.os.Build; + +/** + * @hide + */ +public class ApplicationSuggestion implements Parcelable { + + public static final Creator CREATOR = + new Creator() { + public ApplicationSuggestion createFromParcel(Parcel in) { + return new ApplicationSuggestion(in); + } + + public ApplicationSuggestion[] newArray(int size) { + return new ApplicationSuggestion[size]; + } + }; + + private String mName; + + private String mPackage; + + private Uri mDownloadUri; + + private Uri mThumbnailUri; + + public ApplicationSuggestion(@NonNull String name, @NonNull String pkg, + @NonNull Uri downloadUri, @NonNull Uri thumbnailUri) { + mName = name; + mPackage = pkg; + mDownloadUri = downloadUri; + mThumbnailUri = thumbnailUri; + } + + private ApplicationSuggestion(Parcel in) { + // Read parcelable version, make sure to define explicit changes + // within {@link Build.PARCELABLE_VERSION); + int parcelableVersion = in.readInt(); + int parcelableSize = in.readInt(); + int startPosition = in.dataPosition(); + + if (parcelableVersion >= Build.CM_VERSION_CODES.APRICOT) { + mName = in.readString(); + mPackage = in.readString(); + mDownloadUri = in.readParcelable(Uri.class.getClassLoader()); + mThumbnailUri = in.readParcelable(Uri.class.getClassLoader()); + } + + in.setDataPosition(startPosition + parcelableSize); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + // Write parcelable version, make sure to define explicit changes + // within {@link Build.PARCELABLE_VERSION); + out.writeInt(Build.PARCELABLE_VERSION); + + // Inject a placeholder that will store the parcel size from this point on + // (not including the size itself). + int sizePosition = out.dataPosition(); + out.writeInt(0); + int startPosition = out.dataPosition(); + + out.writeString(mName); + out.writeString(mPackage); + out.writeParcelable(mDownloadUri, flags); + out.writeParcelable(mThumbnailUri, flags); + + // Go back and write size + int parcelableSize = out.dataPosition() - startPosition; + out.setDataPosition(sizePosition); + out.writeInt(parcelableSize); + out.setDataPosition(startPosition + parcelableSize); + } + + public String getName() { + return mName; + } + + public String getPackageName() { + return mPackage; + } + + public Uri getDownloadUri() { + return mDownloadUri; + } + + public Uri getThumbailUri() { + return mThumbnailUri; + } +} diff --git a/src/java/cyanogenmod/app/suggest/IAppSuggestManager.aidl b/src/java/cyanogenmod/app/suggest/IAppSuggestManager.aidl new file mode 100644 index 0000000..68ab87f --- /dev/null +++ b/src/java/cyanogenmod/app/suggest/IAppSuggestManager.aidl @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015, 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 cyanogenmod.app.suggest; + +import android.content.Intent; + +import cyanogenmod.app.suggest.ApplicationSuggestion; + +/** + * @hide + */ +interface IAppSuggestManager { + boolean handles(in Intent intent); + + List getSuggestions(in Intent intent); +} \ No newline at end of file diff --git a/src/java/cyanogenmod/app/suggest/IAppSuggestProvider.aidl b/src/java/cyanogenmod/app/suggest/IAppSuggestProvider.aidl new file mode 100644 index 0000000..759880d --- /dev/null +++ b/src/java/cyanogenmod/app/suggest/IAppSuggestProvider.aidl @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015, 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 cyanogenmod.app.suggest; + +import android.content.Intent; + +import cyanogenmod.app.suggest.ApplicationSuggestion; + +/** + * @hide + */ +interface IAppSuggestProvider { + boolean handles(in Intent intent); + + List getSuggestions(in Intent intent); +} \ No newline at end of file diff --git a/system-api/cm_system-current.txt b/system-api/cm_system-current.txt index 1d0f4f7..daf5fea 100644 --- a/system-api/cm_system-current.txt +++ b/system-api/cm_system-current.txt @@ -463,6 +463,7 @@ package cyanogenmod.platform { public static final class Manifest.permission { ctor public Manifest.permission(); + field public static final java.lang.String ACCESS_APP_SUGGESTIONS = "cyanogenmod.permission.ACCESS_APP_SUGGESTIONS"; field public static final java.lang.String HARDWARE_ABSTRACTION_ACCESS = "cyanogenmod.permission.HARDWARE_ABSTRACTION_ACCESS"; field public static final java.lang.String MANAGE_ALARMS = "cyanogenmod.permission.MANAGE_ALARMS"; field public static final java.lang.String MANAGE_PERSISTENT_STORAGE = "cyanogenmod.permission.MANAGE_PERSISTENT_STORAGE"; @@ -482,10 +483,18 @@ package cyanogenmod.platform { ctor public R(); } + public static final class R.array { + ctor public R.array(); + } + public static final class R.attr { ctor public R.attr(); } + public static final class R.bool { + ctor public R.bool(); + } + public static final class R.drawable { ctor public R.drawable(); }