/* * Copyright (C) 2014 75py * * 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.nagopy.android.xposed.utilities; import java.util.Iterator; import java.util.List; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; import android.content.res.XModuleResources; import android.content.res.XResources; import android.os.Build; import android.os.Bundle; import android.os.UserHandle; import android.text.TextUtils; import android.view.Gravity; import android.view.View; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.CheckBox; import android.widget.GridView; import android.widget.LinearLayout; import android.widget.ListAdapter; import com.nagopy.android.common.util.DimenUtil; import com.nagopy.android.common.util.VersionUtil; import com.nagopy.android.xposed.utilities.XposedModules.InitZygote; import com.nagopy.android.xposed.utilities.XposedModules.XposedModule; import com.nagopy.android.xposed.utilities.setting.AlwaysUsePerAppsList.PerAppsSetting; import com.nagopy.android.xposed.utilities.setting.ModAppPickerSettingsGen; import com.nagopy.android.xposed.utilities.util.Const; import de.robv.android.xposed.IXposedHookZygoteInit.StartupParam; import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XC_MethodReplacement; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedHelpers; import de.robv.android.xposed.callbacks.XC_LayoutInflated; /** * アプリ選択をカスタマイズするモジュール.<br> * 常時のチェックボックス部分はAlternateAppPickerをほぼコピー(Apache License v2.0)。 */ @XposedModule(setting = ModAppPickerSettingsGen.class) public class ModAppPicker { /** アクション名保存用のキー */ private static final String KEY_TARGET_ACTION = "targetAction"; @InitZygote(summary = "常時のチェックボックス表示") public static void showAlwaysCheckBox(final StartupParam startupParam, final ModAppPickerSettingsGen mSettings) { if (!mSettings.showAlwaysUse) { return; } final String resourceName; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { resourceName = "resolver_list"; } else { resourceName = "resolver_grid"; } XModuleResources moduleResources = XModuleResources.createInstance( startupParam.modulePath, null); CharSequence alwaysUse = moduleResources.getText(R.string.alwaysUse); mSettings.alwaysUse = alwaysUse; XResources.hookSystemWideLayout("android", "layout", resourceName, new XC_LayoutInflated() { @Override public void handleLayoutInflated(LayoutInflatedParam liparam) throws Throwable { int id_button_bar = liparam.res.getIdentifier("button_bar", "id", "android"); int id_button_always = liparam.res.getIdentifier("button_bar", "id", "android"); int id_button_once = liparam.res.getIdentifier("button_bar", "id", "android"); int id_resolver = liparam.res.getIdentifier(resourceName, "id", "android"); Context context = liparam.view.getContext(); // 常時、一回のみボタンを非表示にする View buttonAlways = liparam.view.findViewById(id_button_always); View buttonOnce = liparam.view.findViewById(id_button_once); buttonAlways.setVisibility(View.GONE); buttonOnce.setVisibility(View.GONE); // ボタンを入れてるLLを取得 LinearLayout buttonBar = (LinearLayout) liparam.view .findViewById(id_button_bar); // チェックボックスを作成 CheckBox checkBox = new CheckBox(context); checkBox.setChecked(false); checkBox.setText(mSettings.alwaysUse); // LayoutParamsを作成 LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); int fiveDp = DimenUtil.getPixelFromDp(context, 5); layoutParams.setMargins(fiveDp, fiveDp, fiveDp, fiveDp); layoutParams.gravity = Gravity.CENTER_VERTICAL; // チェックボックスを追加 buttonBar.addView(checkBox, 0, layoutParams); // チェックボックス、ボタン部分のLLを表示 checkBox.setVisibility(View.VISIBLE); buttonBar.setVisibility(View.VISIBLE); // ListViewにViewHolderを保存 ViewHolder viewHolder = new ViewHolder(); viewHolder.mAlwaysButton = buttonAlways; viewHolder.mOnceButton = buttonOnce; viewHolder.mAlwaysCheckBox = checkBox; viewHolder.mButtonLayout = buttonBar; View mAbsListView = liparam.view.findViewById(id_resolver); mAbsListView.setTag(R.id.tag_app_picker_view_holder, viewHolder); } }); final Class<?> clsResolverActivity = XposedHelpers.findClass( "com.android.internal.app.ResolverActivity", null); // onIntentSelected XposedHelpers.findAndHookMethod(clsResolverActivity, "onIntentSelected", ResolveInfo.class, Intent.class, boolean.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { param.args[2] = false; } }); XposedHelpers.findAndHookMethod(clsResolverActivity, "onCreate", Bundle.class, Intent.class, CharSequence.class, Intent[].class, java.util.List.class, boolean.class, new XC_MethodHook() { @Override protected void afterHookedMethod(final MethodHookParam param) throws Throwable { final AbsListView mAbsListView = getAbsListView(param.thisObject); ViewHolder viewHolder = (ViewHolder) mAbsListView .getTag(R.id.tag_app_picker_view_holder); final CheckBox mAlwaysCheckBox = viewHolder.mAlwaysCheckBox; mAbsListView.setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView<?> parent, View view, int position, long id) { int checkedPos; if (VersionUtil.isKitKatOrLator()) { checkedPos = mAbsListView.getCheckedItemPosition(); if (checkedPos == AbsListView.INVALID_POSITION) { checkedPos = position; } } else { checkedPos = position; } final boolean enabled = checkedPos != GridView.INVALID_POSITION; if (enabled) { XposedHelpers.callMethod(param.thisObject, "startSelected", position, mAlwaysCheckBox.isChecked()); } } }); } }); } private static AbsListView getAbsListView(Object thisObject) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { return (AbsListView) XposedHelpers.getObjectField(thisObject, "mListView"); } else { return (AbsListView) XposedHelpers.getObjectField(thisObject, "mGrid"); } } /** * ブラックリストのアプリを非表示にする. */ @InitZygote(summary = "ブラックリストのアプリを除外") public static void hideBlackListApps(final StartupParam startupParam, final ModAppPickerSettingsGen mSettings) { if (mSettings.appPickerBlackList == null || mSettings.appPickerBlackList.size() == 0) { return; } // ResolveListAdapter Class<?> clsResolveListAdapter = XposedHelpers.findClass( "com.android.internal.app.ResolverActivity$ResolveListAdapter", null); XposedHelpers.findAndHookMethod(clsResolveListAdapter, "processGroup", List.class, int.class, int.class, ResolveInfo.class, CharSequence.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { List<?> mList = (List<?>) XposedHelpers.getObjectField(param.thisObject, "mList"); Iterator<?> i = mList.iterator(); while (i.hasNext()) { Object obj = i.next(); ResolveInfo ri = (ResolveInfo) XposedHelpers.getObjectField(obj, "ri"); String packageName = ri.activityInfo.packageName; if (mSettings.appPickerBlackList.contains(packageName)) { // ブラックリストに含まれている場合は除外 i.remove(); } } } }); } /** * アプリごとに「常に」状態を保存する. */ @InitZygote(summary = "アプリごとに常時記憶を分離") public static void registerAlwaysCheckPerApps(StartupParam startupParam, final ModAppPickerSettingsGen mSettings) throws Throwable { if (!mSettings.settingAlwaysPerApps) { return; } Class<?> clsResolverActivity = XposedHelpers.findClass( "com.android.internal.app.ResolverActivity", null); XposedHelpers.findAndHookMethod(clsResolverActivity, "onCreate", Bundle.class, Intent.class, CharSequence.class, Intent[].class, java.util.List.class, boolean.class, new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { Activity activity = (Activity) param.thisObject; int mLaunchedFromUid = XposedHelpers.getIntField(param.thisObject, "mLaunchedFromUid"); String launchedFromPkg = activity.getPackageManager().getNameForUid( mLaunchedFromUid); Intent paramIntent = (Intent) param.args[1]; // 呼び出し元のアクションを取得 String targetAction = paramIntent.getAction(); XposedHelpers.setAdditionalInstanceField(activity, KEY_TARGET_ACTION, targetAction); PerAppsSetting launchApp = mSettings.alwaysUsePerApps.findByAction( launchedFromPkg, paramIntent.getAction()); if (launchApp != null) { AbsListView absListView = (AbsListView) getAbsListView(param.thisObject); ListAdapter adapter = absListView.getAdapter(); List<?> mList = (List<?>) XposedHelpers .getObjectField(adapter, "mList"); int childCount = adapter.getCount(); for (int i = 0; i < childCount; i++) { Object item = mList.get(i); ResolveInfo ri = (ResolveInfo) XposedHelpers.getObjectField(item, "ri"); if (TextUtils.equals(ri.activityInfo.packageName, launchApp.targetPackageName) && TextUtils.equals(ri.activityInfo.name, launchApp.targetActivityName)) { absListView.setItemChecked(i, true); XposedHelpers.callMethod(param.thisObject, "startSelected", i, false); break; } } } } }); XposedHelpers.findAndHookMethod(clsResolverActivity, "startSelected", int.class, boolean.class, new XC_MethodReplacement() { @Override protected Object replaceHookedMethod(MethodHookParam param) throws Throwable { int which = (Integer) param.args[0]; boolean always = (Boolean) param.args[1]; if (always) { Activity activity = (Activity) param.thisObject; // 常時の場合、選択したパッケージ名を保存する // 呼び出し元を取得 int mLaunchedFromUid = XposedHelpers.getIntField(param.thisObject, "mLaunchedFromUid"); String launchedFromPkg = activity.getPackageManager().getNameForUid( mLaunchedFromUid); // 選択したアプリを取得 Object mAdapter = XposedHelpers.getObjectField(param.thisObject, "mAdapter"); ResolveInfo ri = (ResolveInfo) XposedHelpers.callMethod(mAdapter, "resolveInfoForPosition", which); String targetPackageName = ri.activityInfo.packageName; String targetActivityName = ri.activityInfo.name; // Actionを取得 Object additionalInstanceField = XposedHelpers .getAdditionalInstanceField(activity, KEY_TARGET_ACTION); String targetAction = additionalInstanceField == null ? "" : (String) additionalInstanceField; // 設定変更のブロードキャストを送信 Intent intent = new Intent(Const.ACTION_ALWAYS_USE_PER_APPS); intent.putExtra(Const.EXTRA_LAUNCHED_FROM_PKG, launchedFromPkg); intent.putExtra(Const.EXTRA_TARGET_PACKAGE_NAME, targetPackageName); intent.putExtra(Const.EXTRA_TARGET_ACTIVITY_NAME, targetActivityName); intent.putExtra(Const.EXTRA_TARGET_ACTION, targetAction); sendBroadcast(activity, intent); // 設定に追加 PerAppsSetting alwaysUsePerApps = new PerAppsSetting(); alwaysUsePerApps.launchedFromPackageName = launchedFromPkg; alwaysUsePerApps.targetPackageName = targetPackageName; alwaysUsePerApps.targetActivityName = targetActivityName; alwaysUsePerApps.targetAction = targetAction; mSettings.alwaysUsePerApps.list.add(alwaysUsePerApps); } // オリジナルを実行 return XposedBridge.invokeOriginalMethod(param.method, param.thisObject, new Object[] { param.args[0], false }); } }); } /** * @param context * @param intent */ @SuppressLint("NewApi") private static void sendBroadcast(Context context, Intent intent) { if (VersionUtil.isJBmr1OrLater()) { UserHandle userAll = (UserHandle) XposedHelpers.getStaticObjectField( UserHandle.class, "ALL"); context.sendBroadcastAsUser(intent, userAll); } else { context.sendBroadcast(intent); } } private static class ViewHolder { LinearLayout mButtonLayout; View mAlwaysButton; View mOnceButton; CheckBox mAlwaysCheckBox; @Override public String toString() { return "ViewHolder [mButtonLayout=" + mButtonLayout + ", mAlwaysButton=" + mAlwaysButton + ", mOnceButton=" + mOnceButton + ", mAlwaysCheckBox=" + mAlwaysCheckBox + "]"; } } }