package com.mopub.common.util; import android.annotation.TargetApi; import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.support.annotation.NonNull; import android.view.Gravity; import android.widget.Toast; import com.mopub.common.Preconditions; import com.mopub.common.VisibleForTesting; import com.mopub.common.logging.MoPubLog; import java.util.ArrayList; import java.util.List; import static com.mopub.common.util.VersionCode.HONEYCOMB_MR2; import static com.mopub.common.util.VersionCode.currentApiLevel; public class ManifestUtils { private ManifestUtils() {} private static final List<Class<? extends Activity>> REQUIRED_WEB_VIEW_SDK_ACTIVITIES; private static FlagCheckUtil sFlagCheckUtil = new FlagCheckUtil(); /** * This class maintains two different lists of required Activity permissions, * for the WebView and Native SDKs. */ static { REQUIRED_WEB_VIEW_SDK_ACTIVITIES = new ArrayList<Class<? extends Activity>>(4); // As a convenience, full class paths are provided here, in case the MoPub SDK was imported // incorrectly and these files were left out. REQUIRED_WEB_VIEW_SDK_ACTIVITIES.add(com.mopub.mobileads.MoPubActivity.class); REQUIRED_WEB_VIEW_SDK_ACTIVITIES.add(com.mopub.mobileads.MraidActivity.class); REQUIRED_WEB_VIEW_SDK_ACTIVITIES.add(com.mopub.mobileads.MraidVideoPlayerActivity.class); REQUIRED_WEB_VIEW_SDK_ACTIVITIES.add(com.mopub.common.MoPubBrowser.class); } private static final List<Class<? extends Activity>> REQUIRED_NATIVE_SDK_ACTIVITIES; static { REQUIRED_NATIVE_SDK_ACTIVITIES = new ArrayList<Class<? extends Activity>>(1); REQUIRED_NATIVE_SDK_ACTIVITIES.add(com.mopub.common.MoPubBrowser.class); } public static void checkWebViewActivitiesDeclared(@NonNull final Context context) { if (!Preconditions.NoThrow.checkNotNull(context, "context is not allowed to be null")) { return; } displayWarningForMissingActivities(context, REQUIRED_WEB_VIEW_SDK_ACTIVITIES); displayWarningForMisconfiguredActivities(context, REQUIRED_WEB_VIEW_SDK_ACTIVITIES); } public static void checkNativeActivitiesDeclared(@NonNull final Context context) { if (!Preconditions.NoThrow.checkNotNull(context, "context is not allowed to be null")) { return; } displayWarningForMissingActivities(context, REQUIRED_NATIVE_SDK_ACTIVITIES); displayWarningForMisconfiguredActivities(context, REQUIRED_NATIVE_SDK_ACTIVITIES); } /** * This method is intended to display a warning to developers when they have accidentally * omitted Activity declarations in their application's AndroidManifest. * Calling this when there are inadequate permissions will always Log a warning to the * developer, and if the the application is debuggable, it will also display a Toast. */ @VisibleForTesting static void displayWarningForMissingActivities(@NonNull final Context context, @NonNull final List<Class<? extends Activity>> requiredActivities) { final List<Class<? extends Activity>> undeclaredActivities = filterDeclaredActivities(context, requiredActivities, false); if (undeclaredActivities.isEmpty()) { return; } logWarningToast(context); // Regardless, log a warning logMissingActivities(undeclaredActivities); } /** * This method is intended to display a warning to developers when they have accidentally * omitted configChanges values from Activity declarations in their application's AndroidManifest. * Calling this when there are inadequate permissions will always Log a warning to the * developer, and if the the application is debuggable, it will also display a Toast. */ @VisibleForTesting static void displayWarningForMisconfiguredActivities(@NonNull final Context context, @NonNull final List<Class<? extends Activity>> requiredActivities) { final List<Class<? extends Activity>> declaredActivities = filterDeclaredActivities(context, requiredActivities, true); final List<Class<? extends Activity>> misconfiguredActivities = getMisconfiguredActivities(context, declaredActivities); if (misconfiguredActivities.isEmpty()) { return; } logWarningToast(context); // Regardless, log a warning logMisconfiguredActivities(context, misconfiguredActivities); } public static boolean isDebuggable(@NonNull final Context context) { final int applicationFlags = context.getApplicationInfo().flags; return Utils.bitMaskContainsFlag(applicationFlags, ApplicationInfo.FLAG_DEBUGGABLE); } /** * Filters in activities to be returned based on matching their declaration state * in the Android Manifest with the isDeclared param. * * @param context * @param requiredActivities activities to filter against * @param isDeclared desired declaration state of activities in Android Manifest to be returned * @return the list of filtered in activities */ private static List<Class<? extends Activity>> filterDeclaredActivities(@NonNull final Context context, @NonNull final List<Class<? extends Activity>> requiredActivities, final boolean isDeclared) { final List<Class<? extends Activity>> activities = new ArrayList<Class<? extends Activity>>(); for (final Class<? extends Activity> activityClass : requiredActivities) { final Intent intent = new Intent(context, activityClass); if (Intents.deviceCanHandleIntent(context, intent) == isDeclared) { activities.add(activityClass); } } return activities; } @TargetApi(13) private static List<Class<? extends Activity>> getMisconfiguredActivities(@NonNull final Context context, @NonNull final List<Class<? extends Activity>> activities) { final List<Class<? extends Activity>> misconfiguredActivities = new ArrayList<Class<? extends Activity>>(); for (final Class<? extends Activity> activity : activities) { ActivityConfigChanges activityConfigChanges; try { activityConfigChanges = getActivityConfigChanges(context, activity); } catch (PackageManager.NameNotFoundException e) { continue; } if (!activityConfigChanges.hasKeyboardHidden || !activityConfigChanges.hasOrientation || !activityConfigChanges.hasScreenSize) { misconfiguredActivities.add(activity); } } return misconfiguredActivities; } private static void logMissingActivities(@NonNull final List<Class<? extends Activity>> undeclaredActivities) { final StringBuilder stringBuilder = new StringBuilder("AndroidManifest permissions for the following required MoPub activities are missing:\n"); for (final Class<? extends Activity> activity : undeclaredActivities) { stringBuilder.append("\n\t").append(activity.getName()); } stringBuilder.append("\n\nPlease update your manifest to include them."); MoPubLog.w(stringBuilder.toString()); } private static void logMisconfiguredActivities(@NonNull Context context, @NonNull final List<Class<? extends Activity>> misconfiguredActivities) { final StringBuilder stringBuilder = new StringBuilder("In AndroidManifest, the android:configChanges param is missing values for the following MoPub activities:\n"); for (final Class<? extends Activity> activity: misconfiguredActivities) { ActivityConfigChanges activityConfigChanges; try { activityConfigChanges = getActivityConfigChanges(context, activity); } catch (PackageManager.NameNotFoundException e) { continue; } if (!activityConfigChanges.hasKeyboardHidden) { stringBuilder.append("\n\tThe android:configChanges param for activity " + activity.getName() + " must include keyboardHidden."); } if (!activityConfigChanges.hasOrientation) { stringBuilder.append("\n\tThe android:configChanges param for activity " + activity.getName() + " must include orientation."); } if (!activityConfigChanges.hasScreenSize) { stringBuilder.append("\n\tThe android:configChanges param for activity " + activity.getName() + " must include screenSize."); } } stringBuilder.append("\n\nPlease update your manifest to include them."); MoPubLog.w(stringBuilder.toString()); } private static ActivityConfigChanges getActivityConfigChanges(@NonNull Context context, @NonNull Class<? extends Activity> activity) throws PackageManager.NameNotFoundException { ActivityInfo activityInfo; // This line can throw NameNotFoundException but we don't expect it to happen since we // should only be operating on declared activities activityInfo = context.getPackageManager() .getActivityInfo(new ComponentName(context, activity.getName()), 0); ActivityConfigChanges activityConfigChanges = new ActivityConfigChanges(); activityConfigChanges.hasKeyboardHidden = sFlagCheckUtil.hasFlag(activity, activityInfo.configChanges, ActivityInfo.CONFIG_KEYBOARD_HIDDEN); activityConfigChanges.hasOrientation = sFlagCheckUtil.hasFlag(activity, activityInfo.configChanges, ActivityInfo.CONFIG_ORIENTATION); activityConfigChanges.hasScreenSize = true; // For screenSize, only set to false if the API level and target API are >= 13 // If the target API is < 13, then Android will implement its own backwards compatibility if (currentApiLevel().isAtLeast(HONEYCOMB_MR2) && context.getApplicationInfo().targetSdkVersion >= VersionCode.HONEYCOMB_MR2.getApiLevel()) { activityConfigChanges.hasScreenSize = sFlagCheckUtil.hasFlag(activity, activityInfo.configChanges, ActivityInfo.CONFIG_SCREEN_SIZE); } return activityConfigChanges; } private static void logWarningToast(@NonNull final Context context) { // If the application is debuggable, display a loud toast if (isDebuggable(context)) { final String message = "ERROR: YOUR MOPUB INTEGRATION IS INCOMPLETE.\n" + "Check logcat and update your AndroidManifest.xml with the correct activities and configuration."; final Toast toast = Toast.makeText(context, message, Toast.LENGTH_LONG); toast.setGravity(Gravity.FILL_HORIZONTAL, 0, 0); toast.show(); } } private static class ActivityConfigChanges { public boolean hasKeyboardHidden; public boolean hasOrientation; public boolean hasScreenSize; } @VisibleForTesting @Deprecated // for testing static List<Class<? extends Activity>> getRequiredWebViewSdkActivities() { return REQUIRED_WEB_VIEW_SDK_ACTIVITIES; } @VisibleForTesting @Deprecated // for testing static List<Class<? extends Activity>> getRequiredNativeSdkActivities() { return REQUIRED_NATIVE_SDK_ACTIVITIES; } @VisibleForTesting @Deprecated // for testing static void setFlagCheckUtil(final FlagCheckUtil flagCheckUtil) { sFlagCheckUtil = flagCheckUtil; } static class FlagCheckUtil { // We're only passing in the Class param here to ease testing and // allow mocks to match on it public boolean hasFlag(@SuppressWarnings("unused") Class clazz, int bitMask, int flag) { return Utils.bitMaskContainsFlag(bitMask, flag); } } }