// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.omaha; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.annotation.TargetApi; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Environment; import android.os.StatFs; import android.text.TextUtils; import android.view.View; import android.view.animation.LinearInterpolator; import org.chromium.base.CommandLine; import org.chromium.base.Log; import org.chromium.base.ThreadUtils; import org.chromium.base.metrics.RecordHistogram; import org.chromium.chrome.R; import org.chromium.chrome.browser.ChromeActivity; import org.chromium.chrome.browser.ChromeSwitches; import org.chromium.chrome.browser.appmenu.AppMenu; import org.chromium.chrome.browser.preferences.PrefServiceBridge; import org.chromium.components.variations.VariationsAssociatedData; import org.chromium.ui.interpolators.BakedBezierInterpolator; import java.io.File; /** * Contains logic for whether the update menu item should be shown, whether the update toolbar badge * should be shown, and UMA logging for the update menu item. */ public class UpdateMenuItemHelper { private static final String TAG = "UpdateMenuItemHelper"; // VariationsAssociatedData configs private static final String FIELD_TRIAL_NAME = "UpdateMenuItem"; private static final String ENABLED_VALUE = "true"; private static final String ENABLE_UPDATE_MENU_ITEM = "enable_update_menu_item"; private static final String ENABLE_UPDATE_BADGE = "enable_update_badge"; private static final String SHOW_SUMMARY = "show_summary"; private static final String USE_NEW_FEATURES_SUMMARY = "use_new_features_summary"; private static final String CUSTOM_SUMMARY = "custom_summary"; // UMA constants for logging whether the menu item was clicked. private static final int ITEM_NOT_CLICKED = 0; private static final int ITEM_CLICKED_INTENT_LAUNCHED = 1; private static final int ITEM_CLICKED_INTENT_FAILED = 2; private static final int ITEM_CLICKED_BOUNDARY = 3; // UMA constants for logging whether Chrome was updated after the menu item was clicked. private static final int UPDATED = 0; private static final int NOT_UPDATED = 1; private static final int UPDATED_BOUNDARY = 2; private static UpdateMenuItemHelper sInstance; private static Object sGetInstanceLock = new Object(); // Whether OmahaClient has already been checked for an update. private boolean mAlreadyCheckedForUpdates; // Whether an update is available. private boolean mUpdateAvailable; // URL to direct the user to when Omaha detects a newer version available. private String mUpdateUrl; // Whether the menu item was clicked. This is used to log the click-through rate. private boolean mMenuItemClicked; // The latest Chrome version available if OmahaClient.isNewerVersionAvailable() returns true. private String mLatestVersion; /** * @return The {@link UpdateMenuItemHelper} instance. */ public static UpdateMenuItemHelper getInstance() { synchronized (UpdateMenuItemHelper.sGetInstanceLock) { if (sInstance == null) { sInstance = new UpdateMenuItemHelper(); String testMarketUrl = getStringParamValue(ChromeSwitches.MARKET_URL_FOR_TESTING); if (!TextUtils.isEmpty(testMarketUrl)) { sInstance.mUpdateUrl = testMarketUrl; } } return sInstance; } } /** * Checks if the {@link OmahaClient} knows about an update. * @param activity The current {@link ChromeActivity}. */ public void checkForUpdateOnBackgroundThread(final ChromeActivity activity) { if (!getBooleanParam(ENABLE_UPDATE_MENU_ITEM) && !getBooleanParam(ChromeSwitches.FORCE_SHOW_UPDATE_MENU_ITEM) && !getBooleanParam(ChromeSwitches.FORCE_SHOW_UPDATE_MENU_BADGE)) { return; } ThreadUtils.assertOnUiThread(); if (mAlreadyCheckedForUpdates) { if (activity.isActivityDestroyed()) return; activity.onCheckForUpdate(mUpdateAvailable); return; } mAlreadyCheckedForUpdates = true; new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { if (OmahaClient.isNewerVersionAvailable(activity)) { mUpdateUrl = OmahaClient.getMarketURL(activity); mLatestVersion = OmahaClient.getLatestVersionNumberString(activity); mUpdateAvailable = true; recordInternalStorageSize(); } else { mUpdateAvailable = false; } return null; } @Override protected void onPostExecute(Void result) { if (activity.isActivityDestroyed()) return; activity.onCheckForUpdate(mUpdateAvailable); recordUpdateHistogram(); } }.execute(); } /** * Logs whether an update was performed if the update menu item was clicked. * Should be called from ChromeActivity#onStart(). */ public void onStart() { if (mAlreadyCheckedForUpdates) { recordUpdateHistogram(); } } /** * @param activity The current {@link ChromeActivity}. * @return Whether the update menu item should be shown. */ public boolean shouldShowMenuItem(ChromeActivity activity) { if (getBooleanParam(ChromeSwitches.FORCE_SHOW_UPDATE_MENU_ITEM)) { return true; } if (!getBooleanParam(ENABLE_UPDATE_MENU_ITEM)) { return false; } return updateAvailable(activity); } /** * @param context The current {@link Context}. * @return The string to use for summary text or the empty string if no summary should be shown. */ public String getMenuItemSummaryText(Context context) { if (!getBooleanParam(SHOW_SUMMARY) && !getBooleanParam(USE_NEW_FEATURES_SUMMARY) && !getBooleanParam(CUSTOM_SUMMARY)) { return ""; } String customSummary = getStringParamValue(CUSTOM_SUMMARY); if (!TextUtils.isEmpty(customSummary)) { return customSummary; } if (getBooleanParam(USE_NEW_FEATURES_SUMMARY)) { return context.getResources().getString(R.string.menu_update_summary_new_features); } return context.getResources().getString(R.string.menu_update_summary_default); } /** * @param activity The current {@link ChromeActivity}. * @return Whether the update badge should be shown in the toolbar. */ public boolean shouldShowToolbarBadge(ChromeActivity activity) { if (getBooleanParam(ChromeSwitches.FORCE_SHOW_UPDATE_MENU_BADGE)) { return true; } // The badge is hidden if the update menu item has been clicked until there is an // even newer version of Chrome available. String latestVersionWhenClicked = PrefServiceBridge.getInstance().getLatestVersionWhenClickedUpdateMenuItem(); if (!getBooleanParam(ENABLE_UPDATE_BADGE) || TextUtils.equals(latestVersionWhenClicked, mLatestVersion)) { return false; } return updateAvailable(activity); } /** * Handles a click on the update menu item. * @param activity The current {@link ChromeActivity}. */ public void onMenuItemClicked(ChromeActivity activity) { if (mUpdateUrl == null) return; // If the update menu item is showing because it was forced on through about://flags // then mLatestVersion may be null. if (mLatestVersion != null) { PrefServiceBridge.getInstance().setLatestVersionWhenClickedUpdateMenuItem( mLatestVersion); } // Fire an intent to open the URL. try { Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(mUpdateUrl)); activity.startActivity(launchIntent); recordItemClickedHistogram(ITEM_CLICKED_INTENT_LAUNCHED); PrefServiceBridge.getInstance().setClickedUpdateMenuItem(true); } catch (ActivityNotFoundException e) { Log.e(TAG, "Failed to launch Activity for: %s", mUpdateUrl); recordItemClickedHistogram(ITEM_CLICKED_INTENT_FAILED); } } /** * Should be called before the AppMenu is dismissed if the update menu item was clicked. */ public void setMenuItemClicked() { mMenuItemClicked = true; } /** * Called when the {@link AppMenu} is dimissed. Logs a histogram immediately if the update menu * item was not clicked. If it was clicked, logging is delayed until #onMenuItemClicked(). */ public void onMenuDismissed() { if (!mMenuItemClicked) { recordItemClickedHistogram(ITEM_NOT_CLICKED); } mMenuItemClicked = false; } /** * Creates an {@link AnimatorSet} for showing the update badge that is displayed on top * of the app menu button. * * @param menuButton The {@link View} containing the app menu button. * @param menuBadge The {@link View} containing the update badge. * @return An {@link AnimatorSet} to run when showing the update badge. */ public static AnimatorSet createShowUpdateBadgeAnimation(final View menuButton, final View menuBadge) { // Create badge ObjectAnimators. ObjectAnimator badgeFadeAnimator = ObjectAnimator.ofFloat(menuBadge, View.ALPHA, 1.f); badgeFadeAnimator.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE); int pixelTranslation = menuBadge.getResources().getDimensionPixelSize( R.dimen.menu_badge_translation_y_distance); ObjectAnimator badgeTranslateYAnimator = ObjectAnimator.ofFloat(menuBadge, View.TRANSLATION_Y, pixelTranslation, 0.f); badgeTranslateYAnimator.setInterpolator(BakedBezierInterpolator.TRANSFORM_CURVE); // Create menu button ObjectAnimator. ObjectAnimator menuButtonFadeAnimator = ObjectAnimator.ofFloat(menuButton, View.ALPHA, 0.f); menuButtonFadeAnimator.setInterpolator(new LinearInterpolator()); // Create AnimatorSet and listeners. AnimatorSet set = new AnimatorSet(); set.playTogether(badgeFadeAnimator, badgeTranslateYAnimator, menuButtonFadeAnimator); set.setDuration(350); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { // Make sure the menu button is visible again. menuButton.setAlpha(1.f); } @Override public void onAnimationCancel(Animator animation) { // Jump to the end state if the animation is canceled. menuBadge.setAlpha(1.f); menuBadge.setTranslationY(0.f); menuButton.setAlpha(1.f); } }); return set; } /** * Creates an {@link AnimatorSet} for hiding the update badge that is displayed on top * of the app menu button. * * @param menuButton The {@link View} containing the app menu button. * @param menuBadge The {@link View} containing the update badge. * @return An {@link AnimatorSet} to run when hiding the update badge. */ public static AnimatorSet createHideUpdateBadgeAnimation(final View menuButton, final View menuBadge) { // Create badge ObjectAnimator. ObjectAnimator badgeFadeAnimator = ObjectAnimator.ofFloat(menuBadge, View.ALPHA, 0.f); badgeFadeAnimator.setInterpolator(BakedBezierInterpolator.FADE_OUT_CURVE); // Create menu button ObjectAnimator. ObjectAnimator menuButtonFadeAnimator = ObjectAnimator.ofFloat(menuButton, View.ALPHA, 1.f); menuButtonFadeAnimator.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE); // Create AnimatorSet and listeners. AnimatorSet set = new AnimatorSet(); set.playTogether(badgeFadeAnimator, menuButtonFadeAnimator); set.setDuration(200); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { menuBadge.setVisibility(View.GONE); } @Override public void onAnimationCancel(Animator animation) { // Jump to the end state if the animation is canceled. menuButton.setAlpha(1.f); menuBadge.setVisibility(View.GONE); } }); return set; } private boolean updateAvailable(ChromeActivity activity) { if (!mAlreadyCheckedForUpdates) { checkForUpdateOnBackgroundThread(activity); return false; } return mUpdateAvailable; } private void recordItemClickedHistogram(int action) { RecordHistogram.recordEnumeratedHistogram("GoogleUpdate.MenuItem.ActionTakenOnMenuOpen", action, ITEM_CLICKED_BOUNDARY); } private void recordUpdateHistogram() { if (PrefServiceBridge.getInstance().getClickedUpdateMenuItem()) { RecordHistogram.recordEnumeratedHistogram( "GoogleUpdate.MenuItem.ActionTakenAfterItemClicked", mUpdateAvailable ? NOT_UPDATED : UPDATED, UPDATED_BOUNDARY); PrefServiceBridge.getInstance().setClickedUpdateMenuItem(false); } } /** * Gets a boolean VariationsAssociatedData parameter, assuming the <paramName>="true" format. * Also checks for a command-line switch with the same name, for easy local testing. * @param paramName The name of the parameter (or command-line switch) to get a value for. * @return Whether the param is defined with a value "true", if there's a command-line * flag present with any value. */ private static boolean getBooleanParam(String paramName) { if (CommandLine.getInstance().hasSwitch(paramName)) { return true; } return TextUtils.equals(ENABLED_VALUE, VariationsAssociatedData.getVariationParamValue(FIELD_TRIAL_NAME, paramName)); } /** * Gets a String VariationsAssociatedData parameter. Also checks for a command-line switch with * the same name, for easy local testing. * @param paramName The name of the parameter (or command-line switch) to get a value for. * @return The command-line flag value if present, or the param is value if present. */ private static String getStringParamValue(String paramName) { String value = CommandLine.getInstance().getSwitchValue(paramName); if (TextUtils.isEmpty(value)) { value = VariationsAssociatedData.getVariationParamValue(FIELD_TRIAL_NAME, paramName); } return value; } private void recordInternalStorageSize() { assert !ThreadUtils.runningOnUiThread(); File path = Environment.getDataDirectory(); StatFs statFs = new StatFs(path.getAbsolutePath()); long size; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { size = getSize(statFs); } else { size = getSizeUpdatedApi(statFs); } RecordHistogram.recordLinearCountHistogram( "GoogleUpdate.InfoBar.InternalStorageSizeAvailable", (int) size, 1, 200, 100); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private static long getSizeUpdatedApi(StatFs statFs) { return statFs.getAvailableBytes() / (1024 * 1024); } @SuppressWarnings("deprecation") private static long getSize(StatFs statFs) { int blockSize = statFs.getBlockSize(); int availableBlocks = statFs.getAvailableBlocks(); return (blockSize * availableBlocks) / (1024 * 1024); } }