Java tutorial
/* * Copyright (C) 2015 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 com.androidinspain.deskclock; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.AlarmManager; import android.app.AlarmManager.AlarmClockInfo; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.Typeface; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Looper; import android.provider.Settings; import android.support.annotation.AnyRes; import android.support.annotation.DrawableRes; import android.support.annotation.StringRes; import android.support.graphics.drawable.VectorDrawableCompat; import android.support.v4.os.BuildCompat; import android.support.v4.view.AccessibilityDelegateCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.DateUtils; import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.util.ArraySet; import android.view.View; import android.widget.TextClock; import android.widget.TextView; import com.androidinspain.deskclock.data.DataModel; import com.androidinspain.deskclock.provider.AlarmInstance; import com.androidinspain.deskclock.uidata.UiDataModel; import java.lang.reflect.Field; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.Locale; import java.util.TimeZone; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY; import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.graphics.Bitmap.Config.ARGB_8888; public class Utils { /** * {@link Uri} signifying the "silent" ringtone. */ public static final Uri RINGTONE_SILENT = Uri.EMPTY; public static void enforceMainLooper() { if (Looper.getMainLooper() != Looper.myLooper()) { throw new IllegalAccessError("May only call from main thread."); } } public static void enforceNotMainLooper() { if (Looper.getMainLooper() == Looper.myLooper()) { throw new IllegalAccessError("May not call from main thread."); } } public static int indexOf(Object[] array, Object item) { for (int i = 0; i < array.length; i++) { if (array[i].equals(item)) { return i; } } return -1; } /** * @return {@code true} if the device is prior to {@link Build.VERSION_CODES#LOLLIPOP} */ public static boolean isPreL() { return Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP; } /** * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP} or * {@link Build.VERSION_CODES#LOLLIPOP_MR1} */ public static boolean isLOrLMR1() { final int sdkInt = Build.VERSION.SDK_INT; return sdkInt == Build.VERSION_CODES.LOLLIPOP || sdkInt == Build.VERSION_CODES.LOLLIPOP_MR1; } /** * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP} or later */ public static boolean isLOrLater() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; } /** * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP_MR1} or later */ public static boolean isLMR1OrLater() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1; } /** * @return {@code true} if the device is {@link Build.VERSION_CODES#M} or later */ public static boolean isMOrLater() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; } /** * @return {@code true} if the device is {@link Build.VERSION_CODES#N} or later */ public static boolean isNOrLater() { return BuildCompat.isAtLeastN(); } /** * @return {@code true} if the device is {@link Build.VERSION_CODES#N_MR1} or later */ public static boolean isNMR1OrLater() { return BuildCompat.isAtLeastNMR1(); } /** * @param resourceId identifies an application resource * @return the Uri by which the application resource is accessed */ public static Uri getResourceUri(Context context, @AnyRes int resourceId) { return new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE).authority(context.getPackageName()) .path(String.valueOf(resourceId)).build(); } /** * @param view the scrollable view to test * @return {@code true} iff the {@code view} content is currently scrolled to the top */ public static boolean isScrolledToTop(View view) { return !view.canScrollVertically(-1); } /** * Calculate the amount by which the radius of a CircleTimerView should be offset by any * of the extra painted objects. */ public static float calculateRadiusOffset(float strokeSize, float dotStrokeSize, float markerStrokeSize) { return Math.max(strokeSize, Math.max(dotStrokeSize, markerStrokeSize)); } /** * Configure the clock that is visible to display seconds. The clock that is not visible never * displays seconds to avoid it scheduling unnecessary ticking runnables. */ public static void setClockSecondsEnabled(TextClock digitalClock, AnalogClock analogClock) { final boolean displaySeconds = DataModel.getDataModel().getDisplayClockSeconds(); final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getClockStyle(); switch (clockStyle) { case ANALOG: setTimeFormat(digitalClock, false); analogClock.enableSeconds(displaySeconds); return; case DIGITAL: analogClock.enableSeconds(false); setTimeFormat(digitalClock, displaySeconds); return; } throw new IllegalStateException("unexpected clock style: " + clockStyle); } /** * Set whether the digital or analog clock should be displayed in the application. * Returns the view to be displayed. */ public static View setClockStyle(View digitalClock, View analogClock) { final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getClockStyle(); switch (clockStyle) { case ANALOG: digitalClock.setVisibility(View.GONE); analogClock.setVisibility(View.VISIBLE); return analogClock; case DIGITAL: digitalClock.setVisibility(View.VISIBLE); analogClock.setVisibility(View.GONE); return digitalClock; } throw new IllegalStateException("unexpected clock style: " + clockStyle); } /** * For screensavers to set whether the digital or analog clock should be displayed. * Returns the view to be displayed. */ public static View setScreensaverClockStyle(View digitalClock, View analogClock) { final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getScreensaverClockStyle(); switch (clockStyle) { case ANALOG: digitalClock.setVisibility(View.GONE); analogClock.setVisibility(View.VISIBLE); return analogClock; case DIGITAL: digitalClock.setVisibility(View.VISIBLE); analogClock.setVisibility(View.GONE); return digitalClock; } throw new IllegalStateException("unexpected clock style: " + clockStyle); } /** * For screensavers to dim the lights if necessary. */ public static void dimClockView(boolean dim, View clockView) { Paint paint = new Paint(); paint.setColor(Color.WHITE); paint.setColorFilter(new PorterDuffColorFilter((dim ? 0x40FFFFFF : 0xC0FFFFFF), PorterDuff.Mode.MULTIPLY)); clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint); } /** * Update and return the PendingIntent corresponding to the given {@code intent}. * * @param context the Context in which the PendingIntent should start the service * @param intent an Intent describing the service to be started * @return a PendingIntent that will start a service */ public static PendingIntent pendingServiceIntent(Context context, Intent intent) { return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT); } /** * Update and return the PendingIntent corresponding to the given {@code intent}. * * @param context the Context in which the PendingIntent should start the activity * @param intent an Intent describing the activity to be started * @return a PendingIntent that will start an activity */ public static PendingIntent pendingActivityIntent(Context context, Intent intent) { return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT); } /** * @return The next alarm from {@link AlarmManager} */ public static String getNextAlarm(Context context) { return isPreL() ? getNextAlarmPreL(context) : getNextAlarmLOrLater(context); } @SuppressWarnings("deprecation") @TargetApi(Build.VERSION_CODES.KITKAT) private static String getNextAlarmPreL(Context context) { final ContentResolver cr = context.getContentResolver(); return Settings.System.getString(cr, Settings.System.NEXT_ALARM_FORMATTED); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static String getNextAlarmLOrLater(Context context) { final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); final AlarmClockInfo info = getNextAlarmClock(am); if (info != null) { final long triggerTime = info.getTriggerTime(); final Calendar alarmTime = Calendar.getInstance(); alarmTime.setTimeInMillis(triggerTime); return AlarmUtils.getFormattedTime(context, alarmTime); } return null; } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static AlarmClockInfo getNextAlarmClock(AlarmManager am) { return am.getNextAlarmClock(); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static void updateNextAlarm(AlarmManager am, AlarmClockInfo info, PendingIntent op) { am.setAlarmClock(info, op); } public static boolean isAlarmWithin24Hours(AlarmInstance alarmInstance) { final Calendar nextAlarmTime = alarmInstance.getAlarmTime(); final long nextAlarmTimeMillis = nextAlarmTime.getTimeInMillis(); return nextAlarmTimeMillis - System.currentTimeMillis() <= DateUtils.DAY_IN_MILLIS; } /** * Clock views can call this to refresh their alarm to the next upcoming value. */ public static void refreshAlarm(Context context, View clock) { final TextView nextAlarmIconView = (TextView) clock.findViewById(R.id.nextAlarmIcon); final TextView nextAlarmView = (TextView) clock.findViewById(R.id.nextAlarm); if (nextAlarmView == null) { return; } final String alarm = getNextAlarm(context); if (!TextUtils.isEmpty(alarm)) { final String description = context.getString(R.string.next_alarm_description, alarm); nextAlarmView.setText(alarm); nextAlarmView.setContentDescription(description); nextAlarmView.setVisibility(View.VISIBLE); nextAlarmIconView.setVisibility(View.VISIBLE); nextAlarmIconView.setContentDescription(description); } else { nextAlarmView.setVisibility(View.GONE); nextAlarmIconView.setVisibility(View.GONE); } } public static void setClockIconTypeface(View clock) { final TextView nextAlarmIconView = (TextView) clock.findViewById(R.id.nextAlarmIcon); nextAlarmIconView.setTypeface(UiDataModel.getUiDataModel().getAlarmIconTypeface()); } /** * Clock views can call this to refresh their date. **/ public static void updateDate(String dateSkeleton, String descriptionSkeleton, View clock) { final TextView dateDisplay = (TextView) clock.findViewById(R.id.date); if (dateDisplay == null) { return; } final Locale l = Locale.getDefault(); final String datePattern = DateFormat.getBestDateTimePattern(l, dateSkeleton); final String descriptionPattern = DateFormat.getBestDateTimePattern(l, descriptionSkeleton); final Date now = new Date(); dateDisplay.setText(new SimpleDateFormat(datePattern, l).format(now)); dateDisplay.setVisibility(View.VISIBLE); dateDisplay.setContentDescription(new SimpleDateFormat(descriptionPattern, l).format(now)); } /*** * Formats the time in the TextClock according to the Locale with a special * formatting treatment for the am/pm label. * * @param clock TextClock to format * @param includeSeconds whether or not to include seconds in the clock's time */ public static void setTimeFormat(TextClock clock, boolean includeSeconds) { if (clock != null) { // Get the best format for 12 hours mode according to the locale clock.setFormat12Hour(get12ModeFormat(0.4f /* amPmRatio */, includeSeconds)); // Get the best format for 24 hours mode according to the locale clock.setFormat24Hour(get24ModeFormat(includeSeconds)); } } /** * @param amPmRatio a value between 0 and 1 that is the ratio of the relative size of the * am/pm string to the time string * @param includeSeconds whether or not to include seconds in the time string * @return format string for 12 hours mode time, not including seconds */ public static CharSequence get12ModeFormat(float amPmRatio, boolean includeSeconds) { String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), includeSeconds ? "hmsa" : "hma"); if (amPmRatio <= 0) { pattern = pattern.replaceAll("a", "").trim(); } // Replace spaces with "Hair Space" pattern = pattern.replaceAll(" ", "\u200A"); // Build a spannable so that the am/pm will be formatted int amPmPos = pattern.indexOf('a'); if (amPmPos == -1) { return pattern; } final Spannable sp = new SpannableString(pattern); sp.setSpan(new RelativeSizeSpan(amPmRatio), amPmPos, amPmPos + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); sp.setSpan(new StyleSpan(Typeface.NORMAL), amPmPos, amPmPos + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); sp.setSpan(new TypefaceSpan("sans-serif"), amPmPos, amPmPos + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return sp; } public static CharSequence get24ModeFormat(boolean includeSeconds) { return DateFormat.getBestDateTimePattern(Locale.getDefault(), includeSeconds ? "Hms" : "Hm"); } /** * Returns string denoting the timezone hour offset (e.g. GMT -8:00) * * @param useShortForm Whether to return a short form of the header that rounds to the * nearest hour and excludes the "GMT" prefix */ public static String getGMTHourOffset(TimeZone timezone, boolean useShortForm) { final int gmtOffset = timezone.getRawOffset(); final long hour = gmtOffset / DateUtils.HOUR_IN_MILLIS; final long min = (Math.abs(gmtOffset) % DateUtils.HOUR_IN_MILLIS) / DateUtils.MINUTE_IN_MILLIS; if (useShortForm) { return String.format(Locale.ENGLISH, "%+d", hour); } else { return String.format(Locale.ENGLISH, "GMT %+d:%02d", hour, min); } } /** * Given a point in time, return the subsequent moment any of the time zones changes days. * e.g. Given 8:00pm on 1/1/2016 and time zones in LA and NY this method would return a Date for * midnight on 1/2/2016 in the NY timezone since it changes days first. * * @param time a point in time from which to compute midnight on the subsequent day * @param zones a collection of time zones * @return the nearest point in the future at which any of the time zones changes days */ public static Date getNextDay(Date time, Collection<TimeZone> zones) { Calendar next = null; for (TimeZone tz : zones) { final Calendar c = Calendar.getInstance(tz); c.setTime(time); // Advance to the next day. c.add(Calendar.DAY_OF_YEAR, 1); // Reset the time to midnight. c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); if (next == null || c.compareTo(next) < 0) { next = c; } } return next == null ? null : next.getTime(); } public static String getNumberFormattedQuantityString(Context context, int id, int quantity) { final String localizedQuantity = NumberFormat.getInstance().format(quantity); return context.getResources().getQuantityString(id, quantity, localizedQuantity); } /** * @return {@code true} iff the widget is being hosted in a container where tapping is allowed */ public static boolean isWidgetClickable(AppWidgetManager widgetManager, int widgetId) { final Bundle wo = widgetManager.getAppWidgetOptions(widgetId); return wo != null && wo.getInt(OPTION_APPWIDGET_HOST_CATEGORY, -1) != WIDGET_CATEGORY_KEYGUARD; } /** * @return a vector-drawable inflated from the given {@code resId} */ public static VectorDrawableCompat getVectorDrawable(Context context, @DrawableRes int resId) { return VectorDrawableCompat.create(context.getResources(), resId, context.getTheme()); } /** * This method assumes the given {@code view} has already been layed out. * * @return a Bitmap containing an image of the {@code view} at its current size */ public static Bitmap createBitmap(View view) { final Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), ARGB_8888); final Canvas canvas = new Canvas(bitmap); view.draw(canvas); return bitmap; } /** * {@link ArraySet} is @hide prior to {@link Build.VERSION_CODES#M}. */ @SuppressLint("NewApi") public static <E> ArraySet<E> newArraySet(Collection<E> collection) { final ArraySet<E> arraySet = new ArraySet<>(collection.size()); arraySet.addAll(collection); return arraySet; } /** * @param context from which to query the current device configuration * @return {@code true} if the device is currently in portrait or reverse portrait orientation */ public static boolean isPortrait(Context context) { return context.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT; } /** * @param context from which to query the current device configuration * @return {@code true} if the device is currently in landscape or reverse landscape orientation */ public static boolean isLandscape(Context context) { return context.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE; } public static long now() { return DataModel.getDataModel().elapsedRealtime(); } public static long wallClock() { return DataModel.getDataModel().currentTimeMillis(); } /** * @param context to obtain strings. * @param displayMinutes whether or not minutes should be included * @param isAhead {@code true} if the time should be marked 'ahead', else 'behind' * @param hoursDifferent the number of hours the time is ahead/behind * @param minutesDifferent the number of minutes the time is ahead/behind * @return String describing the hours/minutes ahead or behind */ public static String createHoursDifferentString(Context context, boolean displayMinutes, boolean isAhead, int hoursDifferent, int minutesDifferent) { String timeString; if (displayMinutes && hoursDifferent != 0) { // Both minutes and hours final String hoursShortQuantityString = Utils.getNumberFormattedQuantityString(context, R.plurals.hours_short, Math.abs(hoursDifferent)); final String minsShortQuantityString = Utils.getNumberFormattedQuantityString(context, R.plurals.minutes_short, Math.abs(minutesDifferent)); final @StringRes int stringType = isAhead ? R.string.world_hours_minutes_ahead : R.string.world_hours_minutes_behind; timeString = context.getString(stringType, hoursShortQuantityString, minsShortQuantityString); } else { // Minutes alone or hours alone final String hoursQuantityString = Utils.getNumberFormattedQuantityString(context, R.plurals.hours, Math.abs(hoursDifferent)); final String minutesQuantityString = Utils.getNumberFormattedQuantityString(context, R.plurals.minutes, Math.abs(minutesDifferent)); final @StringRes int stringType = isAhead ? R.string.world_time_ahead : R.string.world_time_behind; timeString = context.getString(stringType, displayMinutes ? minutesQuantityString : hoursQuantityString); } return timeString; } /** * @param context The context from which to obtain strings * @param hours Hours to display (if any) * @param minutes Minutes to display (if any) * @param seconds Seconds to display * @return Provided time formatted as a String */ static String getTimeString(Context context, int hours, int minutes, int seconds) { if (hours != 0) { return context.getString(R.string.hours_minutes_seconds, hours, minutes, seconds); } if (minutes != 0) { return context.getString(R.string.minutes_seconds, minutes, seconds); } return context.getString(R.string.seconds, seconds); } public static final class ClickAccessibilityDelegate extends AccessibilityDelegateCompat { /** The label for talkback to apply to the view */ private final String mLabel; /** Whether or not to always make the view visible to talkback */ private final boolean mIsAlwaysAccessibilityVisible; public ClickAccessibilityDelegate(String label) { this(label, false); } public ClickAccessibilityDelegate(String label, boolean isAlwaysAccessibilityVisible) { mLabel = label; mIsAlwaysAccessibilityVisible = isAlwaysAccessibilityVisible; } @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(host, info); if (mIsAlwaysAccessibilityVisible) { info.setVisibleToUser(true); } info.addAction(new AccessibilityActionCompat(AccessibilityActionCompat.ACTION_CLICK.getId(), mLabel)); } } public static boolean supportsPowerOffWakeUp() { return getConstantThroughReflection("RTC_POWEROFF_WAKEUP") != -1; } public static int getConstantThroughReflection(String name) { try { Field f = AlarmManager.class.getDeclaredField(name); f.setAccessible(true); return (int) f.get(null); } catch (Exception e) { e.printStackTrace(); return -1; } } }