diff options
Diffstat (limited to 'src/com/android/messaging/util')
69 files changed, 0 insertions, 16090 deletions
diff --git a/src/com/android/messaging/util/AccessibilityUtil.java b/src/com/android/messaging/util/AccessibilityUtil.java deleted file mode 100644 index f6c64a9..0000000 --- a/src/com/android/messaging/util/AccessibilityUtil.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; -import android.content.res.Resources; -import android.support.v4.view.accessibility.AccessibilityEventCompat; -import android.support.v4.view.accessibility.AccessibilityRecordCompat; -import android.text.TextUtils; -import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; - -import com.android.messaging.Factory; -import com.android.messaging.R; - -import javax.annotation.Nullable; - -public class AccessibilityUtil { - public static String sContentDescriptionDivider; - - public static boolean isTouchExplorationEnabled(final Context context) { - final AccessibilityManager accessibilityManager = (AccessibilityManager) - context.getSystemService(Context.ACCESSIBILITY_SERVICE); - return accessibilityManager.isTouchExplorationEnabled(); - } - - public static StringBuilder appendContentDescription(final Context context, - final StringBuilder contentDescription, final String val) { - if (sContentDescriptionDivider == null) { - sContentDescriptionDivider = - context.getResources().getString(R.string.enumeration_comma); - } - if (contentDescription.length() != 0) { - contentDescription.append(sContentDescriptionDivider); - } - contentDescription.append(val); - return contentDescription; - } - - public static void announceForAccessibilityCompat( - final View view, @Nullable final AccessibilityManager accessibilityManager, - final int textResourceId) { - final String text = Factory.get().getApplicationContext().getResources().getString( - textResourceId); - announceForAccessibilityCompat(view, accessibilityManager, text); - } - - public static void announceForAccessibilityCompat( - final View view, @Nullable AccessibilityManager accessibilityManager, - final CharSequence text) { - final Context context = view.getContext().getApplicationContext(); - if (accessibilityManager == null) { - accessibilityManager = (AccessibilityManager) context.getSystemService( - Context.ACCESSIBILITY_SERVICE); - } - - if (!accessibilityManager.isEnabled()) { - return; - } - - // Jelly Bean added support for speaking text verbatim - final int eventType = OsUtil.isAtLeastJB() ? AccessibilityEvent.TYPE_ANNOUNCEMENT - : AccessibilityEvent.TYPE_VIEW_FOCUSED; - - // Construct an accessibility event with the minimum recommended - // attributes. An event without a class name or package may be dropped. - final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); - event.getText().add(text); - event.setEnabled(view.isEnabled()); - event.setClassName(view.getClass().getName()); - event.setPackageName(context.getPackageName()); - - // JellyBean MR1 requires a source view to set the window ID. - final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event); - record.setSource(view); - - // Sends the event directly through the accessibility manager. If we only supported SDK 14+ - // we could have done: - // getParent().requestSendAccessibilityEvent(this, event); - accessibilityManager.sendAccessibilityEvent(event); - } - - /** - * Check to see if the current layout is Right-to-Left. This check is only supported for - * API 17+. - * For earlier versions, this method will just return false. - * @return boolean Boolean indicating whether the currently locale is RTL. - */ - public static boolean isLayoutRtl(final View view) { - if (OsUtil.isAtLeastJB_MR1()) { - return View.LAYOUT_DIRECTION_RTL == view.getLayoutDirection(); - } else { - return false; - } - } - - public static String getVocalizedPhoneNumber(final Resources res, final String phoneNumber) { - if (TextUtils.isEmpty(phoneNumber)) { - return ""; - } - final StringBuilder vocalizedPhoneNumber = new StringBuilder(); - for (final char c : phoneNumber.toCharArray()) { - getVocalizedNumber(res, c, vocalizedPhoneNumber); - } - return vocalizedPhoneNumber.toString(); - } - - public static void getVocalizedNumber(final Resources res, final char c, - final StringBuilder builder) { - switch (c) { - case '0': - builder.append(res.getString(R.string.content_description_for_number_zero)); - builder.append(" "); - return; - case '1': - builder.append(res.getString(R.string.content_description_for_number_one)); - builder.append(" "); - return; - case '2': - builder.append(res.getString(R.string.content_description_for_number_two)); - builder.append(" "); - return; - case '3': - builder.append(res.getString(R.string.content_description_for_number_three)); - builder.append(" "); - return; - case '4': - builder.append(res.getString(R.string.content_description_for_number_four)); - builder.append(" "); - return; - case '5': - builder.append(res.getString(R.string.content_description_for_number_five)); - builder.append(" "); - return; - case '6': - builder.append(res.getString(R.string.content_description_for_number_six)); - builder.append(" "); - return; - case '7': - builder.append(res.getString(R.string.content_description_for_number_seven)); - builder.append(" "); - return; - case '8': - builder.append(res.getString(R.string.content_description_for_number_eight)); - builder.append(" "); - return; - case '9': - builder.append(res.getString(R.string.content_description_for_number_nine)); - builder.append(" "); - return; - default: - builder.append(c); - return; - } - } -} diff --git a/src/com/android/messaging/util/Assert.java b/src/com/android/messaging/util/Assert.java deleted file mode 100644 index 437965c..0000000 --- a/src/com/android/messaging/util/Assert.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.os.Looper; - -import java.util.Arrays; - -public final class Assert { - public static @interface RunsOnMainThread {} - public static @interface DoesNotRunOnMainThread {} - public static @interface RunsOnAnyThread {} - - private static final String TEST_THREAD_SUBSTRING = "test"; - - private static boolean sIsEngBuild; - private static boolean sShouldCrash; - - // Private constructor so no one creates this class. - private Assert() { - } - - // The proguard rules will strip this method out on user/userdebug builds. - // If you change the method signature you MUST edit proguard-release.flags. - private static void setIfEngBuild() { - sShouldCrash = sIsEngBuild = true; - } - - private static void refreshGservices(final BugleGservices gservices) { - sShouldCrash = sIsEngBuild; - if (!sShouldCrash) { - sShouldCrash = gservices.getBoolean( - BugleGservicesKeys.ASSERTS_FATAL, - BugleGservicesKeys.ASSERTS_FATAL_DEFAULT); - } - } - - // Static initializer block to find out if we're running an eng or - // release build. - static { - setIfEngBuild(); - } - - // This is called from FactoryImpl once the Gservices class is initialized. - public static void initializeGservices (final BugleGservices gservices) { - gservices.registerForChanges(new Runnable() { - @Override - public void run() { - refreshGservices(gservices); - } - }); - refreshGservices(gservices); - } - - /** - * Halt execution if this is not an eng build. - * <p>Intended for use in code paths that should be run only for tests and never on - * a real build. - * <p>Note that this will crash on a user build even though asserts don't normally - * crash on a user build. - */ - public static void isEngBuild() { - isTrueReleaseCheck(sIsEngBuild); - } - - /** - * Halt execution if this isn't the case. - */ - public static void isTrue(final boolean condition) { - if (!condition) { - fail("Expected condition to be true", false); - } - } - - /** - * Halt execution if this isn't the case. - */ - public static void isFalse(final boolean condition) { - if (condition) { - fail("Expected condition to be false", false); - } - } - - /** - * Halt execution even in release builds if this isn't the case. - */ - public static void isTrueReleaseCheck(final boolean condition) { - if (!condition) { - fail("Expected condition to be true", true); - } - } - - public static void equals(final int expected, final int actual) { - if (expected != actual) { - fail("Expected " + expected + " but got " + actual, false); - } - } - - public static void equals(final long expected, final long actual) { - if (expected != actual) { - fail("Expected " + expected + " but got " + actual, false); - } - } - - public static void equals(final Object expected, final Object actual) { - if (expected != actual - && (expected == null || actual == null || !expected.equals(actual))) { - fail("Expected " + expected + " but got " + actual, false); - } - } - - public static void oneOf(final int actual, final int ...expected) { - for (int value : expected) { - if (actual == value) { - return; - } - } - fail("Expected value to be one of " + Arrays.toString(expected) + " but was " + actual); - } - - public static void inRange( - final int val, final int rangeMinInclusive, final int rangeMaxInclusive) { - if (val < rangeMinInclusive || val > rangeMaxInclusive) { - fail("Expected value in range [" + rangeMinInclusive + ", " + - rangeMaxInclusive + "], but was " + val, false); - } - } - - public static void inRange( - final long val, final long rangeMinInclusive, final long rangeMaxInclusive) { - if (val < rangeMinInclusive || val > rangeMaxInclusive) { - fail("Expected value in range [" + rangeMinInclusive + ", " + - rangeMaxInclusive + "], but was " + val, false); - } - } - - public static void isMainThread() { - if (Looper.myLooper() != Looper.getMainLooper() - && !Thread.currentThread().getName().contains(TEST_THREAD_SUBSTRING)) { - fail("Expected to run on main thread", false); - } - } - - public static void isNotMainThread() { - if (Looper.myLooper() == Looper.getMainLooper() - && !Thread.currentThread().getName().contains(TEST_THREAD_SUBSTRING)) { - fail("Not expected to run on main thread", false); - } - } - - /** - * Halt execution if the value passed in is not null - * @param obj The object to check - */ - public static void isNull(final Object obj) { - if (obj != null) { - fail("Expected object to be null", false); - } - } - - /** - * Halt execution if the value passed in is not null - * @param obj The object to check - * @param failureMessage message to print when halting execution - */ - public static void isNull(final Object obj, final String failureMessage) { - if (obj != null) { - fail(failureMessage, false); - } - } - - /** - * Halt execution if the value passed in is null - * @param obj The object to check - */ - public static void notNull(final Object obj) { - if (obj == null) { - fail("Expected value to be non-null", false); - } - } - - public static void fail(final String message) { - fail("Assert.fail() called: " + message, false); - } - - private static void fail(final String message, final boolean crashRelease) { - LogUtil.e(LogUtil.BUGLE_TAG, message); - if (crashRelease || sShouldCrash) { - throw new AssertionError(message); - } else { - // Find the method whose assertion failed. We're using a depth of 2, because all public - // Assert methods delegate to this one (see javadoc on getCaller() for details). - StackTraceElement caller = DebugUtils.getCaller(2); - if (caller != null) { - // This log message can be de-obfuscated by the Proguard retrace tool, just like a - // full stack trace from a crash. - LogUtil.e(LogUtil.BUGLE_TAG, "\tat " + caller.toString()); - } - } - } -} diff --git a/src/com/android/messaging/util/AvatarUriUtil.java b/src/com/android/messaging/util/AvatarUriUtil.java deleted file mode 100644 index df7d085..0000000 --- a/src/com/android/messaging/util/AvatarUriUtil.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.graphics.Color; -import android.net.Uri; -import android.net.Uri.Builder; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; - -import com.android.messaging.datamodel.data.ParticipantData; - -import java.util.ArrayList; -import java.util.List; - -/** - * A helper utility for creating {@link android.net.Uri}s to describe what avatar to fetch or - * generate and will help verify and extract information from avatar {@link android.net.Uri}s. - * - * There are three types of avatar {@link android.net.Uri}. - * - * 1) Group Avatars - These are avatars which are used to represent a group conversation. Group - * avatars uris are basically multiple avatar uri which can be any of the below types but not - * another group avatar. The group avatars can hold anywhere from two to four avatars uri and can - * be in any of the following format - * messaging://avatar/g?p=<avatarUri>&p=<avatarUri2> - * messaging://avatar/g?p=<avatarUri>&p=<avatarUri2>&p=<avatarUri3> - * messaging://avatar/g?p=<avatarUri>&p=<avatarUri2>&p=<avatarUri3>&p=<avatarUri4> - * - * 2) Local Resource - A local resource avatar is use when there is a profile photo for the - * participant. This can be any local resource. - * - * 3) Letter Tile - A letter tile is used when a participant has a name but no profile photo. A - * letter tile will contain the first code point of the participant's name and a background color - * based on the hash of the participant's full name. Letter tiles will be in the following format. - * messaging://avatar/l?n=<fullName> - * - * 4) Default Avatars - These are avatars are used when the participant has no profile photo or - * name. In these cases we use the default person icon with a color background. The color - * background is based on a hash of the normalized phone number. - * - * 5) Default Background Avatars - This is a special case for Default Avatars where we use the - * default background color for the default avatar. - * - * 6) SIM Selector Avatars - These are avatars used in the SIM selector. This may either be a - * regular local resource avatar (2) or an avatar with a SIM identifier (i.e. SIM background with - * a letter or a slot number). - */ -public class AvatarUriUtil { - private static final int MAX_GROUP_PARTICIPANTS = 4; - - public static final String TYPE_GROUP_URI = "g"; - public static final String TYPE_LOCAL_RESOURCE_URI = "r"; - public static final String TYPE_LETTER_TILE_URI = "l"; - public static final String TYPE_DEFAULT_URI = "d"; - public static final String TYPE_DEFAULT_BACKGROUND_URI = "b"; - public static final String TYPE_SIM_SELECTOR_URI = "s"; - - private static final String SCHEME = "messaging"; - private static final String AUTHORITY = "avatar"; - private static final String PARAM_NAME = "n"; - private static final String PARAM_PRIMARY_URI = "m"; - private static final String PARAM_FALLBACK_URI = "f"; - private static final String PARAM_PARTICIPANT = "p"; - private static final String PARAM_IDENTIFIER = "i"; - private static final String PARAM_SIM_COLOR = "c"; - private static final String PARAM_SIM_SELECTED = "s"; - private static final String PARAM_SIM_INCOMING = "g"; - - public static final Uri DEFAULT_BACKGROUND_AVATAR = new Uri.Builder().scheme(SCHEME) - .authority(AUTHORITY).appendPath(TYPE_DEFAULT_BACKGROUND_URI).build(); - - private static final Uri BLANK_SIM_INDICATOR_INCOMING_URI = createSimIconUri("", - false /* selected */, Color.TRANSPARENT, true /* incoming */); - private static final Uri BLANK_SIM_INDICATOR_OUTGOING_URI = createSimIconUri("", - false /* selected */, Color.TRANSPARENT, false /* incoming */); - - /** - * Creates an avatar uri based on a list of ParticipantData. The list of participants may not - * be null or empty. Depending on the size of the list either a group avatar uri will be create - * or an individual's avatar will be created. This will never return a null uri. - */ - public static Uri createAvatarUri(@NonNull final List<ParticipantData> participants) { - Assert.notNull(participants); - Assert.isTrue(!participants.isEmpty()); - - if (participants.size() == 1) { - return createAvatarUri(participants.get(0)); - } - - final int numParticipants = Math.min(participants.size(), MAX_GROUP_PARTICIPANTS); - final ArrayList<Uri> avatarUris = new ArrayList<Uri>(numParticipants); - for (int i = 0; i < numParticipants; i++) { - avatarUris.add(createAvatarUri(participants.get(i))); - } - return AvatarUriUtil.joinAvatarUriToGroup(avatarUris); - } - - /** - * Joins together a list of valid avatar uri into a group uri.The list of participants may not - * be null or empty. If a lit of one is given then the first element will be return back - * instead of a group avatar uri. All uris in the list must be a valid avatar uri. This will - * never return a null uri. - */ - public static Uri joinAvatarUriToGroup(@NonNull final List<Uri> avatarUris) { - Assert.notNull(avatarUris); - Assert.isTrue(!avatarUris.isEmpty()); - - if (avatarUris.size() == 1) { - final Uri firstAvatar = avatarUris.get(0); - Assert.isTrue(AvatarUriUtil.isAvatarUri(firstAvatar)); - return firstAvatar; - } - - final Builder builder = new Builder(); - builder.scheme(SCHEME); - builder.authority(AUTHORITY); - builder.appendPath(TYPE_GROUP_URI); - final int numParticipants = Math.min(avatarUris.size(), MAX_GROUP_PARTICIPANTS); - for (int i = 0; i < numParticipants; i++) { - final Uri uri = avatarUris.get(i); - Assert.notNull(uri); - Assert.isTrue(UriUtil.isLocalResourceUri(uri) || AvatarUriUtil.isAvatarUri(uri)); - builder.appendQueryParameter(PARAM_PARTICIPANT, uri.toString()); - } - return builder.build(); - } - - /** - * Creates an avatar uri based on ParticipantData which may not be null and expected to have - * profilePhotoUri, fullName and normalizedDestination populated. This will never return a null - * uri. - */ - public static Uri createAvatarUri(@NonNull final ParticipantData participant) { - Assert.notNull(participant); - final String photoUriString = participant.getProfilePhotoUri(); - final Uri profilePhotoUri = (photoUriString == null) ? null : Uri.parse(photoUriString); - final String name = participant.getFullName(); - final String destination = participant.getNormalizedDestination(); - final String contactLookupKey = participant.getLookupKey(); - return createAvatarUri(profilePhotoUri, name, destination, contactLookupKey); - } - - /** - * Creates an avatar uri based on a the input data. - */ - public static Uri createAvatarUri(final Uri profilePhotoUri, final CharSequence name, - final String defaultIdentifier, final String contactLookupKey) { - Uri generatedUri; - if (!TextUtils.isEmpty(name) && isValidFirstCharacter(name)) { - generatedUri = AvatarUriUtil.fromName(name, contactLookupKey); - } else { - final String identifier = TextUtils.isEmpty(contactLookupKey) - ? defaultIdentifier : contactLookupKey; - generatedUri = AvatarUriUtil.fromIdentifier(identifier); - } - - if (profilePhotoUri != null) { - if (UriUtil.isLocalResourceUri(profilePhotoUri)) { - return fromLocalResourceWithFallback(profilePhotoUri, generatedUri); - } else { - return profilePhotoUri; - } - } else { - return generatedUri; - } - } - - public static boolean isValidFirstCharacter(final CharSequence name) { - final char c = name.charAt(0); - return c != '+'; - } - - /** - * Creates an avatar URI used for the SIM selector. - * @param participantData the self participant data for an <i>active</i> SIM - * @param slotIdentifier when null, this will simply use a regular avatar; otherwise, the - * first letter of slotIdentifier will be used for the icon. - * @param selected is this the currently selected SIM? - * @param incoming is this for an incoming message or outgoing message? - */ - public static Uri createAvatarUri(@NonNull final ParticipantData participantData, - @Nullable final String slotIdentifier, final boolean selected, final boolean incoming) { - Assert.notNull(participantData); - Assert.isTrue(participantData.isActiveSubscription()); - Assert.isTrue(!TextUtils.isEmpty(slotIdentifier) || - !TextUtils.isEmpty(participantData.getProfilePhotoUri())); - if (TextUtils.isEmpty(slotIdentifier)) { - return createAvatarUri(participantData); - } - - return createSimIconUri(slotIdentifier, selected, participantData.getSubscriptionColor(), - incoming); - } - - private static Uri createSimIconUri(final String slotIdentifier, final boolean selected, - final int subColor, final boolean incoming) { - final Builder builder = new Builder(); - builder.scheme(SCHEME); - builder.authority(AUTHORITY); - builder.appendPath(TYPE_SIM_SELECTOR_URI); - builder.appendQueryParameter(PARAM_IDENTIFIER, slotIdentifier); - builder.appendQueryParameter(PARAM_SIM_COLOR, String.valueOf(subColor)); - builder.appendQueryParameter(PARAM_SIM_SELECTED, String.valueOf(selected)); - builder.appendQueryParameter(PARAM_SIM_INCOMING, String.valueOf(incoming)); - return builder.build(); - } - - public static Uri getBlankSimIndicatorUri(final boolean incoming) { - return incoming ? BLANK_SIM_INDICATOR_INCOMING_URI : BLANK_SIM_INDICATOR_OUTGOING_URI; - } - - /** - * Creates an avatar uri from the given local resource Uri, followed by a fallback Uri in case - * the local resource one could not be loaded. - */ - private static Uri fromLocalResourceWithFallback(@NonNull final Uri profilePhotoUri, - @NonNull Uri fallbackUri) { - Assert.notNull(profilePhotoUri); - Assert.notNull(fallbackUri); - final Builder builder = new Builder(); - builder.scheme(SCHEME); - builder.authority(AUTHORITY); - builder.appendPath(TYPE_LOCAL_RESOURCE_URI); - builder.appendQueryParameter(PARAM_PRIMARY_URI, profilePhotoUri.toString()); - builder.appendQueryParameter(PARAM_FALLBACK_URI, fallbackUri.toString()); - return builder.build(); - } - - private static Uri fromName(@NonNull final CharSequence name, final String contactLookupKey) { - Assert.notNull(name); - final Builder builder = new Builder(); - builder.scheme(SCHEME); - builder.authority(AUTHORITY); - builder.appendPath(TYPE_LETTER_TILE_URI); - final String nameString = String.valueOf(name); - builder.appendQueryParameter(PARAM_NAME, nameString); - final String identifier = - TextUtils.isEmpty(contactLookupKey) ? nameString : contactLookupKey; - builder.appendQueryParameter(PARAM_IDENTIFIER, identifier); - return builder.build(); - } - - private static Uri fromIdentifier(@NonNull final String identifier) { - final Builder builder = new Builder(); - builder.scheme(SCHEME); - builder.authority(AUTHORITY); - builder.appendPath(TYPE_DEFAULT_URI); - builder.appendQueryParameter(PARAM_IDENTIFIER, identifier); - return builder.build(); - } - - public static boolean isAvatarUri(@NonNull final Uri uri) { - Assert.notNull(uri); - return uri != null && TextUtils.equals(SCHEME, uri.getScheme()) && - TextUtils.equals(AUTHORITY, uri.getAuthority()); - } - - public static String getAvatarType(@NonNull final Uri uri) { - Assert.notNull(uri); - final List<String> path = uri.getPathSegments(); - return path.isEmpty() ? null : path.get(0); - } - - public static String getIdentifier(@NonNull final Uri uri) { - Assert.notNull(uri); - return uri.getQueryParameter(PARAM_IDENTIFIER); - } - - public static String getName(@NonNull final Uri uri) { - Assert.notNull(uri); - return uri.getQueryParameter(PARAM_NAME); - } - - public static List<String> getGroupParticipantUris(@NonNull final Uri uri) { - Assert.notNull(uri); - return uri.getQueryParameters(PARAM_PARTICIPANT); - } - - public static int getSimColor(@NonNull final Uri uri) { - Assert.notNull(uri); - return Integer.valueOf(uri.getQueryParameter(PARAM_SIM_COLOR)); - } - - public static boolean getSimSelected(@NonNull final Uri uri) { - Assert.notNull(uri); - return Boolean.valueOf(uri.getQueryParameter(PARAM_SIM_SELECTED)); - } - - public static boolean getSimIncoming(@NonNull final Uri uri) { - Assert.notNull(uri); - return Boolean.valueOf(uri.getQueryParameter(PARAM_SIM_INCOMING)); - } - - public static Uri getPrimaryUri(@NonNull final Uri uri) { - Assert.notNull(uri); - final String primaryUriString = uri.getQueryParameter(PARAM_PRIMARY_URI); - return primaryUriString == null ? null : Uri.parse(primaryUriString); - } - - public static Uri getFallbackUri(@NonNull final Uri uri) { - Assert.notNull(uri); - final String fallbackUriString = uri.getQueryParameter(PARAM_FALLBACK_URI); - return fallbackUriString == null ? null : Uri.parse(fallbackUriString); - } -} diff --git a/src/com/android/messaging/util/BugleActivityUtil.java b/src/com/android/messaging/util/BugleActivityUtil.java deleted file mode 100644 index 7f722fd..0000000 --- a/src/com/android/messaging/util/BugleActivityUtil.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.os.UserManager; -import android.text.TextUtils; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.datamodel.DataModel; -import com.android.messaging.ui.conversation.ConversationActivity; -import com.android.messaging.ui.conversationlist.ConversationListActivity; - -/** - * Utility class including logic to verify requirements to run Bugle and other activity startup - * logic. Called from base Bugle activity classes. - */ -public class BugleActivityUtil { - - private static final int REQUEST_GOOGLE_PLAY_SERVICES = 0; - - /** - * Determine if the requirements for the app to run are met. Log any Activity startup - * analytics. - * @param context - * @param activity is used to launch an error Dialog if necessary - * @return true if resume should continue normally. Returns false if some requirements to run - * are not met. - */ - public static boolean onActivityResume(Context context, Activity activity) { - DataModel.get().onActivityResume(); - Factory.get().onActivityResume(); - - // Validate all requirements to run are met - return checkHasSmsPermissionsForUser(context, activity); - } - - /** - * Determine if the user doesn't have SMS permissions. This can happen if you are not the phone - * owner and the owner has disabled your SMS permissions. - * @param context is the Context used to resolve the user permissions - * @param activity is the Activity used to launch an error Dialog if necessary - * @return true if the user has SMS permissions, otherwise false. - */ - private static boolean checkHasSmsPermissionsForUser(Context context, Activity activity) { - if (!OsUtil.isAtLeastL()) { - // UserManager.DISALLOW_SMS added in L. No multiuser phones before this - return true; - } - UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); - if (userManager.hasUserRestriction(UserManager.DISALLOW_SMS)) { - new AlertDialog.Builder(activity) - .setMessage(R.string.requires_sms_permissions_message) - .setCancelable(false) - .setNegativeButton(R.string.requires_sms_permissions_close_button, - new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, - final int button) { - System.exit(0); - } - }) - .show(); - return false; - } - return true; - } -} - diff --git a/src/com/android/messaging/util/BugleApplicationPrefs.java b/src/com/android/messaging/util/BugleApplicationPrefs.java deleted file mode 100644 index e9fceb4..0000000 --- a/src/com/android/messaging/util/BugleApplicationPrefs.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; - -/** - * Provides interface to access application-wide shared preferences. This includes both the user - * visible preferences (e.g. the general settings in the settings page), and internal preferences - * under {@link BuglePrefsKeys}. - */ -public class BugleApplicationPrefs extends BuglePrefsImpl { - public BugleApplicationPrefs(Context context) { - super(context); - } - - @Override - public String getSharedPreferencesName() { - return SHARED_PREFERENCES_NAME; - } - - @Override - protected void validateKey(String key) { - super.validateKey(key); - // Callers shouldn't try to access per-subscription preferences from this class - Assert.isFalse(key.startsWith(SHARED_PREFERENCES_PER_SUBSCRIPTION_PREFIX)); - } - - @Override - public void onUpgrade(int oldVersion, int newVersion) { - } -} diff --git a/src/com/android/messaging/util/BugleGservices.java b/src/com/android/messaging/util/BugleGservices.java deleted file mode 100644 index 2e095de..0000000 --- a/src/com/android/messaging/util/BugleGservices.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.android.messaging.util; - -import com.android.messaging.Factory; - -/** - * A thin wrapper for getting GServices value. During constructor time a one time background thread - * will cache all GServices key with the prefix of "bugle_". All get calls will wait for Gservices - * to finish caching the first time. In practice, the background thread will finish before any get - * request. - */ -public abstract class BugleGservices { - static final String BUGLE_GSERVICES_PREFIX = "bugle_"; - - public static BugleGservices get() { - return Factory.get().getBugleGservices(); - } - - public abstract void registerForChanges(final Runnable r); - - /** - * @param key The key to look up in GServices - * @param defaultValue The default value if value in GServices is null or if - * NumberFormatException is caught. - * @return The corresponding value, or the default value. - */ - public abstract long getLong(final String key, final long defaultValue); - - /** - * @param key The key to look up in GServices - * @param defaultValue The default value if value in GServices is null or if - * NumberFormatException is caught. - * @return The corresponding value, or the default value. - */ - public abstract int getInt(final String key, final int defaultValue); - - /** - * @param key The key to look up in GServices - * @param defaultValue The default value if value in GServices is null. - * @return The corresponding value, or the default value. - */ - public abstract boolean getBoolean(final String key, final boolean defaultValue); - - /** - * @param key The key to look up in GServices - * @param defaultValue The default value if value in GServices is null. - * @return The corresponding value, or the default value. - */ - public abstract String getString(final String key, final String defaultValue); - - /** - * @param key The key to look up in GServices - * @param defaultValue The default value if value in GServices is null. - * @return The corresponding value, or the default value. - */ - public abstract float getFloat(final String key, final float defaultValue); -} diff --git a/src/com/android/messaging/util/BugleGservicesImpl.java b/src/com/android/messaging/util/BugleGservicesImpl.java deleted file mode 100644 index 5ef0898..0000000 --- a/src/com/android/messaging/util/BugleGservicesImpl.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; - -/** - * A thin wrapper for getting GServices value. - */ -public class BugleGservicesImpl extends BugleGservices { - public BugleGservicesImpl(final Context context) { - } - - @Override - public void registerForChanges(final Runnable r) { - } - - /** - * Asserts that the key has the expected prefix. - */ - private void assertKeyAndWaitForGservices(final String key) { - Assert.isTrue(key.startsWith(BUGLE_GSERVICES_PREFIX)); - } - - @Override - public long getLong(final String key, final long defaultValue) { - assertKeyAndWaitForGservices(key); - return defaultValue; - } - - @Override - public int getInt(final String key, final int defaultValue) { - assertKeyAndWaitForGservices(key); - return defaultValue; - } - - @Override - public boolean getBoolean(final String key, final boolean defaultValue) { - assertKeyAndWaitForGservices(key); - return defaultValue; - } - - @Override - public String getString(final String key, final String defaultValue) { - assertKeyAndWaitForGservices(key); - return defaultValue; - } - - @Override - public float getFloat(final String key, final float defaultValue) { - assertKeyAndWaitForGservices(key); - return defaultValue; - } -} diff --git a/src/com/android/messaging/util/BugleGservicesKeys.java b/src/com/android/messaging/util/BugleGservicesKeys.java deleted file mode 100644 index f36dd7f..0000000 --- a/src/com/android/messaging/util/BugleGservicesKeys.java +++ /dev/null @@ -1,298 +0,0 @@ -/* - * 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.android.messaging.util; - - -/** - * List of gservices keys and default values which are in use. - */ -public final class BugleGservicesKeys { - private BugleGservicesKeys() {} // do not instantiate - - /** - * Whether to enable extra debugging features on the client. Default is - * {@value #ENABLE_DEBUGGING_FEATURES_DEFAULT}. - */ - public static final String ENABLE_DEBUGGING_FEATURES - = "bugle_debugging"; - public static final boolean ENABLE_DEBUGGING_FEATURES_DEFAULT - = false; - - /** - * Whether to enable saving extra logs. Default is {@value #ENABLE_LOG_SAVER_DEFAULT}. - */ - public static final String ENABLE_LOG_SAVER = "bugle_logsaver"; - public static final boolean ENABLE_LOG_SAVER_DEFAULT = false; - - /** - * Time in milliseconds of initial (attempt 1) resend backoff for failing messages - */ - public static final String INITIAL_MESSAGE_RESEND_DELAY_MS = "bugle_resend_delay_in_millis"; - public static final long INITIAL_MESSAGE_RESEND_DELAY_MS_DEFAULT = 5 * 1000L; - - /** - * Time in milliseconds of max resend backoff for failing messages - */ - public static final String MAX_MESSAGE_RESEND_DELAY_MS = "bugle_max_resend_delay_in_millis"; - public static final long MAX_MESSAGE_RESEND_DELAY_MS_DEFAULT = 2 * 60 * 60 * 1000L; - - /** - * Time in milliseconds of resend window for unsent messages - */ - public static final String MESSAGE_RESEND_TIMEOUT_MS = "bugle_resend_timeout_in_millis"; - public static final long MESSAGE_RESEND_TIMEOUT_MS_DEFAULT = 20 * 60 * 1000L; - - /** - * Time in milliseconds of download window for new mms notifications - */ - public static final String MESSAGE_DOWNLOAD_TIMEOUT_MS = "bugle_download_timeout_in_millis"; - public static final long MESSAGE_DOWNLOAD_TIMEOUT_MS_DEFAULT = 20 * 60 * 1000L; - - /** - * Time in milliseconds for SMS send timeout - */ - public static final String SMS_SEND_TIMEOUT_IN_MILLIS = "bugle_sms_send_timeout"; - public static final long SMS_SEND_TIMEOUT_IN_MILLIS_DEFAULT = 5 * 60 * 1000L; - - /** - * Keys to control the SMS sync batch size. The batch size is defined by the number - * of messages that incur local database change, e.g. importing messages and - * deleting messages. - * - * 1. The minimum size for a batch and - * 2. The maximum size for a batch. - * The first batch uses the minimum size for probing. Set this to a small number for the - * first sync batch to make sure the user sees SMS showing up in conversations quickly - * Use these two settings to limit the number of messages to sync in each batch. - * The minimum is to make sure we always make progress during sync. The maximum is - * to limit the sync batch size within a reasonable range (needs to fit in an intent). - * 3. The time limit controls the limit of time duration of a sync batch. We can - * not control this directly due to the batching nature of sync. So this provides - * heuristics. We may sometime exceeds the limit if our calculation is off due to - * whatever reasons. Keeping this low ensures responsiveness of the application. - * 4. The limit on number of total messages to scan in one batch. - */ - public static final String SMS_SYNC_BATCH_SIZE_MIN = - "bugle_sms_sync_batch_size_min"; - public static final int SMS_SYNC_BATCH_SIZE_MIN_DEFAULT = 80; - public static final String SMS_SYNC_BATCH_SIZE_MAX = - "bugle_sms_sync_batch_size_max"; - public static final int SMS_SYNC_BATCH_SIZE_MAX_DEFAULT = 1000; - public static final String SMS_SYNC_BATCH_TIME_LIMIT_MILLIS = - "bugle_sms_sync_batch_time_limit"; - public static final long SMS_SYNC_BATCH_TIME_LIMIT_MILLIS_DEFAULT = 400; - public static final String SMS_SYNC_BATCH_MAX_MESSAGES_TO_SCAN = - "bugle_sms_sync_batch_max_messages_to_scan"; - public static final int SMS_SYNC_BATCH_MAX_MESSAGES_TO_SCAN_DEFAULT = - SMS_SYNC_BATCH_SIZE_MAX_DEFAULT * 4; - - /** - * Time in ms for sync to backoff from "now" to the latest message that will be sync'd. - * - * This controls the best case for how out of date the application will appear to be - * when bringing in changes made outside the application. It also represents a buffer - * to ensure that sync doesn't trigger based on changes made within the application. - */ - public static final String SMS_SYNC_BACKOFF_TIME_MILLIS = - "bugle_sms_sync_backoff_time"; - public static final long SMS_SYNC_BACKOFF_TIME_MILLIS_DEFAULT = 5000L; - - /** - * Just in case if we fall into a loop of full sync -> still not synchronized -> full sync ... - * This forces a backoff time so that we at most do full sync once a while (an hour by default) - */ - public static final String SMS_FULL_SYNC_BACKOFF_TIME_MILLIS = - "bugle_sms_full_sync_backoff_time"; - public static final long SMS_FULL_SYNC_BACKOFF_TIME_MILLIS_DEFAULT = 60 * 60 * 1000; - - /** - * Time duration to retain the most recent SMS messages for SMS storage purging - * - * Format: - * <number>(w|m|y) - * Examples: - * "1y" -- a year - * "2w" -- two weeks - * "6m" -- six months - */ - public static final String SMS_STORAGE_PURGING_MESSAGE_RETAINING_DURATION = - "bugle_sms_storage_purging_message_retaining_duration"; - public static final String SMS_STORAGE_PURGING_MESSAGE_RETAINING_DURATION_DEFAULT = "1m"; - - /** - * MMS UA profile url. - * - * This is used on all Android devices running Hangout, so cannot just host the profile of the - * latest and greatest phones. However, if we're on KitKat or below we can't get the phone's - * UA profile and thus we need to send them the default url. - */ - public static final String MMS_UA_PROFILE_URL = - "bugle_mms_uaprofurl"; - public static final String MMS_UA_PROFILE_URL_DEFAULT = - "http://www.gstatic.com/android/sms/mms_ua_profile.xml"; - - /** - * MMS apn mmsc - */ - public static final String MMS_MMSC = - "bugle_mms_mmsc"; - - /** - * MMS apn proxy ip address - */ - public static final String MMS_PROXY_ADDRESS = - "bugle_mms_proxy_address"; - - /** - * MMS apn proxy port - */ - public static final String MMS_PROXY_PORT = - "bugle_mms_proxy_port"; - - /** - * List of known SMS system messages that we will ignore (no deliver, no abort) so that the - * user doesn't see them and the appropriate app is able to handle them. We are delivering - * these as a \n delimited list of patterns, however we should eventually move to storing - * them with the per-carrier mms config xml file. - */ - public static final String SMS_IGNORE_MESSAGE_REGEX = - "bugle_sms_ignore_message_regex"; - public static final String SMS_IGNORE_MESSAGE_REGEX_DEFAULT = ""; - - /** - * When receiving or importing an mms, limit the length of text to this limit. Huge blocks - * of text can cause the app to hang/ANR/or crash in native text code.. - */ - public static final String MMS_TEXT_LIMIT = "bugle_mms_text_limit"; - public static final int MMS_TEXT_LIMIT_DEFAULT = 2000; - - /** - * Max number of attachments the user may add to a single message. - */ - public static final String MMS_ATTACHMENT_LIMIT = "bugle_mms_attachment_limit"; - public static final int MMS_ATTACHMENT_LIMIT_DEFAULT = 10; - - /** - * The max number of messages to show in a single conversation notification. We always show - * the most recent message. If this value is >1, we may also include prior messages as well. - */ - public static final String MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION = - "bugle_max_messages_in_conversation_notification"; - public static final int MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION_DEFAULT = 7; - - /** - * Time (in seconds) between notification ringing for incoming messages of the same - * conversation. We won't ding more often than this value for messages coming in at a high rate. - */ - public static final String NOTIFICATION_TIME_BETWEEN_RINGS_SECONDS - = "bugle_notification_time_between_rings_seconds"; - public static final int NOTIFICATION_TIME_BETWEEN_RINGS_SECONDS_DEFAULT = 10; - - /** - * The max number of messages to show in a single conversation notification, when a wearable - * device (i.e. smartwatch) is paired with the phone. Watches have a different UX model and - * less screen real estate, so we may want to optimize for that case. Note that if a wearable - * is paired, this value will apply to notifications as shown both on the watch and the phone. - */ - public static final String MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION_WITH_WEARABLE = - "bugle_max_messages_in_conversation_notification_with_wearable"; - public static final int MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION_WITH_WEARABLE_DEFAULT = 1; - - /** - * Regular expression to match against query. If it matches then display - * the query plan for this query. - */ - public static final String EXPLAIN_QUERY_PLAN_REGEXP = "bugle_query_plan_regexp"; - - /** - * Whether asserts are fatal on user/userdebug builds. - * Default is {@value #ASSERTS_FATAL_DEFAULT}. - */ - public static final String ASSERTS_FATAL = "bugle_asserts_fatal"; - public static final boolean ASSERTS_FATAL_DEFAULT = false; - - /** - * Whether to use API for sending/downloading MMS (if present, true for L). - * Default is {@value #USE_MMS_API_IF_PRESENT_DEFAULT}. - */ - public static final String USE_MMS_API_IF_PRESENT = "bugle_use_mms_api"; - public static final boolean USE_MMS_API_IF_PRESENT_DEFAULT = true; - - /** - * Whether to always auto-complete email addresses for sending MMS. By default, Bugle starts - * to auto-complete after the user has typed the "@" character. - * Default is (@value ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS_DEFAULT}. - */ - public static final String ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS = - "bugle_always_autocomplete_email_address"; - public static final boolean ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS_DEFAULT = false; - - // We typically request an aspect ratio close the the screen size, but some cameras can be - // flaky and not work well in certain aspect ratios. This allows us to guide the CameraManager - // to pick a more reliable aspect ratio. The value is a float like 1.333f or 1.777f. There is - // no hard coded default because the default is the screen aspect ratio. - public static final String CAMERA_ASPECT_RATIO = "bugle_camera_aspect_ratio"; - - /** - * The recent time range within which we should check MMS WAP Push duplication - * If the value is 0, it signals that we should use old dedup algorithm for wap push - */ - public static final String MMS_WAP_PUSH_DEDUP_TIME_LIMIT_SECS = - "bugle_mms_wap_push_dedup_time_limit_secs"; - public static final long MMS_WAP_PUSH_DEDUP_TIME_LIMIT_SECS_DEFAULT = 7 * 24 * 3600; // 7 days - - /** - * Whether to use persistent, on-disk LogSaver - */ - public static final String PERSISTENT_LOGSAVER = "bugle_persistent_logsaver"; - public static final boolean PERSISTENT_LOGSAVER_DEFAULT = false; - - /** - * For in-memory LogSaver, what's the size of memory buffer in number of records - */ - public static final String IN_MEMORY_LOGSAVER_RECORD_COUNT = - "bugle_in_memory_logsaver_record_count"; - public static final int IN_MEMORY_LOGSAVER_RECORD_COUNT_DEFAULT = 500; - - /** - * For on-disk LogSaver, what's the size of file rotation set - */ - public static final String PERSISTENT_LOGSAVER_ROTATION_SET_SIZE = - "bugle_persistent_logsaver_rotation_set_size"; - public static final int PERSISTENT_LOGSAVER_ROTATION_SET_SIZE_DEFAULT = 8; - - /** - * For on-disk LogSaver, what's the byte limit of a single log file - */ - public static final String PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES = - "bugle_persistent_logsaver_file_limit"; - public static final int PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES_DEFAULT = 256 * 1024; // 256KB - - /** - * We concatenate all text parts in an MMS to form the message text. This specifies - * the separator between the combinated text parts. Default is ' ' (space). - */ - public static final String MMS_TEXT_CONCAT_SEPARATOR = "bugle_mms_text_concat_separator"; - public static final String MMS_TEXT_CONCAT_SEPARATOR_DEFAULT = " "; - - /** - * Whether to enable transcoding GIFs. We sometimes need to compress GIFs to make them small - * enough to send via MMS (which often limits messages to 1 MB in size). - */ - public static final String ENABLE_GIF_TRANSCODING = "bugle_gif_transcoding"; - public static final boolean ENABLE_GIF_TRANSCODING_DEFAULT = true; -} diff --git a/src/com/android/messaging/util/BuglePrefs.java b/src/com/android/messaging/util/BuglePrefs.java deleted file mode 100644 index 74a0d46..0000000 --- a/src/com/android/messaging/util/BuglePrefs.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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.android.messaging.util; - -import com.android.messaging.Factory; - -/** - * Thin wrapper to get/set shared prefs values. - */ -public abstract class BuglePrefs { - /** - * Shared preferences name for preferences applicable to the entire app. - */ - public static final String SHARED_PREFERENCES_NAME = "bugle"; - - /** - * Shared preferences name for subscription-specific preferences. - * Note: for all subscription-specific preferences, please prefix the shared preference keys - * with "buglesub_", so that Bugle may perform runtime validations on preferences to make sure - * you don't accidentally write per-subscription settings into the general pref file, and vice - * versa. - */ - public static final String SHARED_PREFERENCES_PER_SUBSCRIPTION_PREFIX = "buglesub_"; - - /** - * A placeholder base version for Bugle builds where no shared pref version was defined. - */ - public static final int NO_SHARED_PREFERENCES_VERSION = -1; - - /** - * Returns the shared preferences file name to use. - * Subclasses should override and return the shared preferences file. - */ - public abstract String getSharedPreferencesName(); - - - /** - * Handles pref version upgrade. - */ - public abstract void onUpgrade(final int oldVersion, final int newVersion); - - /** - * Gets the SharedPreferences accessor to the application-wide preferences. - */ - public static BuglePrefs getApplicationPrefs() { - return Factory.get().getApplicationPrefs(); - } - - /** - * Gets the SharedPreferences accessor to the subscription-specific preferences. - */ - public static BuglePrefs getSubscriptionPrefs(final int subId) { - return Factory.get().getSubscriptionPrefs(subId); - } - - /** - * @param key The key to look up in shared prefs - * @param defaultValue The default value if value in shared prefs is null or if - * NumberFormatException is caught. - * @return The corresponding value, or the default value. - */ - public abstract int getInt(final String key, final int defaultValue); - - /** - * @param key The key to look up in shared prefs - * @param defaultValue The default value if value in shared prefs is null or if - * NumberFormatException is caught. - * @return The corresponding value, or the default value. - */ - public abstract long getLong(final String key, final long defaultValue); - - /** - * @param key The key to look up in shared prefs - * @param defaultValue The default value if value in shared prefs is null. - * @return The corresponding value, or the default value. - */ - public abstract boolean getBoolean(final String key, final boolean defaultValue); - - /** - * @param key The key to look up in shared prefs - * @param defaultValue The default value if value in shared prefs is null. - * @return The corresponding value, or the default value. - */ - public abstract String getString(final String key, final String defaultValue); - - /** - * @param key The key to look up in shared prefs - * @return The corresponding value, or null if not found. - */ - public abstract byte[] getBytes(final String key); - - /** - * @param key The key to set in shared prefs - * @param value The value to assign to the key - */ - public abstract void putInt(final String key, final int value); - - /** - * @param key The key to set in shared prefs - * @param value The value to assign to the key - */ - public abstract void putLong(final String key, final long value); - - /** - * @param key The key to set in shared prefs - * @param value The value to assign to the key - */ - public abstract void putBoolean(final String key, final boolean value); - - /** - * @param key The key to set in shared prefs - * @param value The value to assign to the key - */ - public abstract void putString(final String key, final String value); - - /** - * @param key The key to set in shared prefs - * @param value The value to assign to the key - */ - public abstract void putBytes(final String key, final byte[] value); - - /** - * @param key The key to remove from shared prefs - */ - public abstract void remove(String key); -} diff --git a/src/com/android/messaging/util/BuglePrefsImpl.java b/src/com/android/messaging/util/BuglePrefsImpl.java deleted file mode 100644 index 7563040..0000000 --- a/src/com/android/messaging/util/BuglePrefsImpl.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Base64; - -/** - * Thin wrapper to get/set shared prefs values. - */ -public abstract class BuglePrefsImpl extends BuglePrefs { - - private final Context mContext; - - public BuglePrefsImpl(final Context context) { - mContext = context; - } - - /** - * Validate the prefs key passed in. Subclasses should override this as needed to perform - * runtime checks (such as making sure per-subscription settings don't sneak into application- - * wide settings). - */ - protected void validateKey(String key) { - } - - @Override - public int getInt(final String key, final int defaultValue) { - validateKey(key); - final SharedPreferences prefs = mContext.getSharedPreferences( - getSharedPreferencesName(), Context.MODE_PRIVATE); - return prefs.getInt(key, defaultValue); - } - - @Override - public long getLong(final String key, final long defaultValue) { - validateKey(key); - final SharedPreferences prefs = mContext.getSharedPreferences( - getSharedPreferencesName(), Context.MODE_PRIVATE); - return prefs.getLong(key, defaultValue); - } - - @Override - public boolean getBoolean(final String key, final boolean defaultValue) { - validateKey(key); - final SharedPreferences prefs = mContext.getSharedPreferences( - getSharedPreferencesName(), Context.MODE_PRIVATE); - return prefs.getBoolean(key, defaultValue); - } - - @Override - public String getString(final String key, final String defaultValue) { - validateKey(key); - final SharedPreferences prefs = mContext.getSharedPreferences( - getSharedPreferencesName(), Context.MODE_PRIVATE); - return prefs.getString(key, defaultValue); - } - - @Override - public byte[] getBytes(String key) { - final String byteValue = getString(key, null); - return byteValue == null ? null : Base64.decode(byteValue, Base64.DEFAULT); - } - - @Override - public void putInt(final String key, final int value) { - validateKey(key); - final SharedPreferences prefs = mContext.getSharedPreferences( - getSharedPreferencesName(), Context.MODE_PRIVATE); - final SharedPreferences.Editor editor = prefs.edit(); - editor.putInt(key, value); - editor.apply(); - } - - @Override - public void putLong(final String key, final long value) { - validateKey(key); - final SharedPreferences prefs = mContext.getSharedPreferences( - getSharedPreferencesName(), Context.MODE_PRIVATE); - final SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(key, value); - editor.apply(); - } - - @Override - public void putBoolean(final String key, final boolean value) { - validateKey(key); - final SharedPreferences prefs = mContext.getSharedPreferences( - getSharedPreferencesName(), Context.MODE_PRIVATE); - final SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(key, value); - editor.apply(); - } - - @Override - public void putString(final String key, final String value) { - validateKey(key); - final SharedPreferences prefs = mContext.getSharedPreferences( - getSharedPreferencesName(), Context.MODE_PRIVATE); - final SharedPreferences.Editor editor = prefs.edit(); - editor.putString(key, value); - editor.apply(); - } - - @Override - public void putBytes(String key, byte[] value) { - final String encodedBytes = Base64.encodeToString(value, Base64.DEFAULT); - putString(key, encodedBytes); - } - - @Override - public void remove(final String key) { - validateKey(key); - final SharedPreferences prefs = mContext.getSharedPreferences( - getSharedPreferencesName(), Context.MODE_PRIVATE); - final SharedPreferences.Editor editor = prefs.edit(); - editor.remove(key); - editor.apply(); - } -} diff --git a/src/com/android/messaging/util/BuglePrefsKeys.java b/src/com/android/messaging/util/BuglePrefsKeys.java deleted file mode 100644 index ae409bc..0000000 --- a/src/com/android/messaging/util/BuglePrefsKeys.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.android.messaging.util; - -/** - * List of shared preferences keys and default values. These are all internal - * (not user-visible) preferences. Preferences that are exposed via the Settings - * activity should be defined in the constants.xml resource file instead. - */ -public final class BuglePrefsKeys { - private BuglePrefsKeys() {} // do not instantiate - - /** - * Bugle's shared preferences version - */ - public static final String SHARED_PREFERENCES_VERSION = - "shared_preferences_version"; - public static final int SHARED_PREFERENCES_VERSION_DEFAULT = - BuglePrefs.NO_SHARED_PREFERENCES_VERSION; - - /** - * Last time that we ran a a sync (in millis) - */ - public static final String LAST_SYNC_TIME - = "last_sync_time_millis"; - public static final long LAST_SYNC_TIME_DEFAULT - = -1; - - /** - * Last time that we ran a full sync (in millis) - */ - public static final String LAST_FULL_SYNC_TIME - = "last_full_sync_time_millis"; - public static final long LAST_FULL_SYNC_TIME_DEFAULT - = -1; - - /** - * Timestamp of the message for which we last did a message notification. - */ - public static final String LATEST_NOTIFICATION_MESSAGE_TIMESTAMP - = "latest_notification_message_timestamp"; - - /** - * The last selected chooser index in the media picker. - */ - public static final String SELECTED_MEDIA_PICKER_CHOOSER_INDEX - = "selected_media_picker_chooser_index"; - public static final int SELECTED_MEDIA_PICKER_CHOOSER_INDEX_DEFAULT - = -1; - - /** - * The attempt number when retrying ProcessPendingMessagesAction - */ - public static final String PROCESS_PENDING_MESSAGES_RETRY_COUNT - = "process_pending_retry"; - -} diff --git a/src/com/android/messaging/util/BugleSubscriptionPrefs.java b/src/com/android/messaging/util/BugleSubscriptionPrefs.java deleted file mode 100644 index 039712a..0000000 --- a/src/com/android/messaging/util/BugleSubscriptionPrefs.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; -import android.content.res.Resources; -import android.text.TextUtils; - -import com.android.messaging.Factory; -import com.android.messaging.R; - -/** - * Provides interface to access per-subscription shared preferences. We have one instance of - * this per active subscription. - */ -public class BugleSubscriptionPrefs extends BuglePrefsImpl { - private final int mSubId; - - public BugleSubscriptionPrefs(final Context context, final int subId) { - super(context); - mSubId = subId; - } - - @Override - public String getSharedPreferencesName() { - return SHARED_PREFERENCES_PER_SUBSCRIPTION_PREFIX + String.valueOf(mSubId); - } - - @Override - protected void validateKey(String key) { - super.validateKey(key); - // Callers should only access per-subscription preferences from this class - Assert.isTrue(key.startsWith(SHARED_PREFERENCES_PER_SUBSCRIPTION_PREFIX)); - } - - @Override - public void onUpgrade(final int oldVersion, final int newVersion) { - switch (oldVersion) { - case BuglePrefs.NO_SHARED_PREFERENCES_VERSION: - // Upgrade to version 1. Adding per-subscription shared prefs. - // Migrate values from the application-wide settings. - migratePrefBooleanInternal(BuglePrefs.getApplicationPrefs(), "delivery_reports", - R.string.delivery_reports_pref_key, R.bool.delivery_reports_pref_default); - migratePrefBooleanInternal(BuglePrefs.getApplicationPrefs(), "auto_retrieve_mms", - R.string.auto_retrieve_mms_pref_key, R.bool.auto_retrieve_mms_pref_default); - migratePrefBooleanInternal(BuglePrefs.getApplicationPrefs(), - "auto_retrieve_mms_when_roaming", - R.string.auto_retrieve_mms_when_roaming_pref_key, - R.bool.auto_retrieve_mms_when_roaming_pref_default); - migratePrefBooleanInternal(BuglePrefs.getApplicationPrefs(), "group_messaging", - R.string.group_mms_pref_key, R.bool.group_mms_pref_default); - - if (PhoneUtils.getDefault().getActiveSubscriptionCount() == 1) { - migratePrefStringInternal(BuglePrefs.getApplicationPrefs(), "mms_phone_number", - R.string.mms_phone_number_pref_key, null); - } - } - } - - private void migratePrefBooleanInternal(final BuglePrefs oldPrefs, final String oldKey, - final int newKeyResId, final int defaultValueResId) { - final Resources resources = Factory.get().getApplicationContext().getResources(); - final boolean defaultValue = resources.getBoolean(defaultValueResId); - final boolean oldValue = oldPrefs.getBoolean(oldKey, defaultValue); - - // Only migrate pref value if it's different than the default. - if (oldValue != defaultValue) { - putBoolean(resources.getString(newKeyResId), oldValue); - } - } - - private void migratePrefStringInternal(final BuglePrefs oldPrefs, final String oldKey, - final int newKeyResId, final String defaultValue) { - final Resources resources = Factory.get().getApplicationContext().getResources(); - final String oldValue = oldPrefs.getString(oldKey, defaultValue); - - // Only migrate pref value if it's different than the default. - if (!TextUtils.equals(oldValue, defaultValue)) { - putString(resources.getString(newKeyResId), oldValue); - } - } -} diff --git a/src/com/android/messaging/util/BugleWidgetPrefs.java b/src/com/android/messaging/util/BugleWidgetPrefs.java deleted file mode 100644 index 63ba567..0000000 --- a/src/com/android/messaging/util/BugleWidgetPrefs.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; - -/** - * Provides interface to access shared preferences used by bugle widgets. - */ -public class BugleWidgetPrefs extends BuglePrefsImpl { - /** - * Shared preferences name for preferences applicable to the entire app. - */ - public static final String SHARED_PREFERENCES_WIDGET_NAME = "bugle_widgets"; - - public BugleWidgetPrefs(Context context) { - super(context); - } - - @Override - public String getSharedPreferencesName() { - return SHARED_PREFERENCES_WIDGET_NAME; - } - - @Override - public void onUpgrade(int oldVersion, int newVersion) { - } -} diff --git a/src/com/android/messaging/util/ChangeDefaultSmsAppHelper.java b/src/com/android/messaging/util/ChangeDefaultSmsAppHelper.java deleted file mode 100644 index 6cf2f25..0000000 --- a/src/com/android/messaging/util/ChangeDefaultSmsAppHelper.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.app.Activity; -import android.app.Fragment; -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.view.View; - -import com.android.messaging.R; -import com.android.messaging.ui.SnackBar; -import com.android.messaging.ui.UIIntents; - -public class ChangeDefaultSmsAppHelper { - private Runnable mRunAfterMadeDefault; - private ChangeSmsAppSettingRunnable mChangeSmsAppSettingRunnable; - - private static final int REQUEST_SET_DEFAULT_SMS_APP = 1; - - /** - * When there's some condition that prevents an operation, such as sending a message, - * call warnOfMissingActionConditions to put up a toast and allow the user to repair - * that condition. - * @param sending - true if we're called during a sending operation - * @param runAfterMadeDefault - a runnable to run after the user responds - * positively to the condition prompt and resolves the condition. It is - * preferable to specify the value in {@link #handleChangeDefaultSmsResult} - * as that handles the case where the process gets restarted. - * If null, the user will be shown a generic toast message. - * @param composeView - compose view that may have the keyboard opened and focused - * @param rootView - if non-null, use this to attach a snackBar - * @param activity - calling activity - * @param fragment - calling fragment, may be null if called directly from an activity - */ - public void warnOfMissingActionConditions(final boolean sending, - final Runnable runAfterMadeDefault, - final View composeView, final View rootView, - final Activity activity, final Fragment fragment) { - final PhoneUtils phoneUtils = PhoneUtils.getDefault(); - final boolean isSmsCapable = phoneUtils.isSmsCapable(); - final boolean hasPreferredSmsSim = phoneUtils.getHasPreferredSmsSim(); - final boolean isDefaultSmsApp = phoneUtils.isDefaultSmsApp(); - - // Supports SMS? - if (!isSmsCapable) { - UiUtils.showToast(R.string.sms_disabled); - - // Has a preferred sim? - } else if (!hasPreferredSmsSim) { - UiUtils.showToast(R.string.no_preferred_sim_selected); - - // Is the default sms app? - } else if (!isDefaultSmsApp) { - mChangeSmsAppSettingRunnable = new ChangeSmsAppSettingRunnable(activity, fragment); - promptToChangeDefaultSmsApp(sending, runAfterMadeDefault, - composeView, rootView, activity); - } - - LogUtil.w(LogUtil.BUGLE_TAG, "Unsatisfied action condition: " - + "isSmsCapable=" + isSmsCapable + ", " - + "hasPreferredSmsSim=" + hasPreferredSmsSim + ", " - + "isDefaultSmsApp=" + isDefaultSmsApp); - } - - private void promptToChangeDefaultSmsApp(final boolean sending, - final Runnable runAfterMadeDefault, - final View composeView, final View rootView, - final Activity activity) { - if (composeView != null) { - // Avoid bug in system which puts soft keyboard over dialog after orientation change - ImeUtil.hideSoftInput(activity, composeView); - } - mRunAfterMadeDefault = runAfterMadeDefault; - - if (rootView == null) { - // Immediately open the system "Change default SMS app?" dialog setting. - mChangeSmsAppSettingRunnable.run(); - } else { - UiUtils.showSnackBarWithCustomAction(activity, - rootView, - activity.getString(sending ? R.string.requires_default_sms_app_to_send : - R.string.requires_default_sms_app), - SnackBar.Action.createCustomAction(mChangeSmsAppSettingRunnable, - activity.getString(R.string.requires_default_sms_change_button)), - null /* interactions */, - SnackBar.Placement.above(composeView)); - } - } - - private class ChangeSmsAppSettingRunnable implements Runnable { - private final Activity mActivity; - private final Fragment mFragment; - - public ChangeSmsAppSettingRunnable(final Activity activity, final Fragment fragment) { - mActivity = activity; - mFragment = fragment; - } - - @Override - public void run() { - try { - final Intent intent = UIIntents.get().getChangeDefaultSmsAppIntent(mActivity); - if (mFragment != null) { - mFragment.startActivityForResult(intent, REQUEST_SET_DEFAULT_SMS_APP); - } else { - mActivity.startActivityForResult(intent, REQUEST_SET_DEFAULT_SMS_APP); - } - } catch (final ActivityNotFoundException ex) { - // We shouldn't get here, but the monkey on JB MR0 can trigger it. - LogUtil.w(LogUtil.BUGLE_TAG, "Couldn't find activity:", ex); - UiUtils.showToastAtBottom(R.string.activity_not_found_message); - } - } - } - - public void handleChangeDefaultSmsResult( - final int requestCode, - final int resultCode, - Runnable runAfterMadeDefault) { - Assert.isTrue(mRunAfterMadeDefault == null || runAfterMadeDefault == null); - if (runAfterMadeDefault == null) { - runAfterMadeDefault = mRunAfterMadeDefault; - } - - if (requestCode == REQUEST_SET_DEFAULT_SMS_APP) { - if (resultCode == Activity.RESULT_OK) { - // mRunAfterMadeDefault can be null if it was set only in - // promptToChangeDefaultSmsApp, and the process subsequently restarted when the - // user momentarily switched to another app. In that case, we'll simply show a - // generic toast since we do not know what the runnable was supposed to do. - if (runAfterMadeDefault != null) { - runAfterMadeDefault.run(); - } else { - UiUtils.showToast(R.string.toast_after_setting_default_sms_app); - } - } - mRunAfterMadeDefault = null; // don't want to accidentally run it again - } - } -} - - diff --git a/src/com/android/messaging/util/CircularArray.java b/src/com/android/messaging/util/CircularArray.java deleted file mode 100644 index db6cf12..0000000 --- a/src/com/android/messaging/util/CircularArray.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.android.messaging.util; - -/** - * Very simple circular array implementation. - * - * @param <E> The element type of this list. - * @LibraryInternal - */ -public class CircularArray<E> { - private int mNextWriter; - private boolean mHasWrapped; - private int mMaxCount; - Object mList[]; - - /** - * Constructor for CircularArray. - * - * @param count Max elements to hold in the list. - */ - public CircularArray(int count) { - mMaxCount = count; - clear(); - } - - /** - * Reset the list. - */ - public void clear() { - mNextWriter = 0; - mHasWrapped = false; - mList = new Object[mMaxCount]; - } - - /** - * Add an element to the end of the list. - * - * @param object The object to add. - */ - public void add(E object) { - mList[mNextWriter] = object; - ++mNextWriter; - if (mNextWriter == mMaxCount) { - mNextWriter = 0; - mHasWrapped = true; - } - } - - /** - * Get the number of elements in the list. This will be 0 <= returned count <= max count - * - * @return Elements in the circular list. - */ - public int count() { - if (mHasWrapped) { - return mMaxCount; - } else { - return mNextWriter; - } - } - - /** - * Return null if the list hasn't wrapped yet. Otherwise return the next object that would be - * overwritten. Can be useful to avoid extra allocations. - * - * @return - */ - @SuppressWarnings("unchecked") - public E getFree() { - if (!mHasWrapped) { - return null; - } else { - return (E) mList[mNextWriter]; - } - } - - /** - * Get the object at index. Index 0 is the oldest item inserted into the list. Index (count() - - * 1) is the newest. - * - * @param index Index to retrieve. - * @return Object at index. - */ - @SuppressWarnings("unchecked") - public E get(int index) { - if (mHasWrapped) { - int wrappedIndex = index + mNextWriter; - if (wrappedIndex >= mMaxCount) { - wrappedIndex -= mMaxCount; - } - return (E) mList[wrappedIndex]; - } else { - return (E) mList[index]; - } - } -} diff --git a/src/com/android/messaging/util/ConnectivityUtil.java b/src/com/android/messaging/util/ConnectivityUtil.java deleted file mode 100644 index 49f6e0a..0000000 --- a/src/com/android/messaging/util/ConnectivityUtil.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.net.ConnectivityManager; -import android.telephony.PhoneStateListener; -import android.telephony.ServiceState; -import android.telephony.SignalStrength; -import android.telephony.TelephonyManager; - -public class ConnectivityUtil { - // Assume not connected until informed differently - private volatile int mCurrentServiceState = ServiceState.STATE_POWER_OFF; - - private final TelephonyManager mTelephonyManager; - private final Context mContext; - private final ConnectivityBroadcastReceiver mReceiver; - private final ConnectivityManager mConnMgr; - - private ConnectivityListener mListener; - private final IntentFilter mIntentFilter; - - public interface ConnectivityListener { - public void onConnectivityStateChanged(final Context context, final Intent intent); - public void onPhoneStateChanged(final Context context, int serviceState); - } - - public ConnectivityUtil(final Context context) { - mContext = context; - mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - mConnMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - mReceiver = new ConnectivityBroadcastReceiver(); - mIntentFilter = new IntentFilter(); - mIntentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); - } - - public int getCurrentServiceState() { - return mCurrentServiceState; - } - - private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() { - @Override - public void onServiceStateChanged(final ServiceState serviceState) { - if (mCurrentServiceState != serviceState.getState()) { - mCurrentServiceState = serviceState.getState(); - onPhoneStateChanged(mCurrentServiceState); - } - } - - @Override - public void onDataConnectionStateChanged(final int state) { - mCurrentServiceState = (state == TelephonyManager.DATA_DISCONNECTED) ? - ServiceState.STATE_OUT_OF_SERVICE : ServiceState.STATE_IN_SERVICE; - } - }; - - private void onPhoneStateChanged(final int serviceState) { - final ConnectivityListener listener = mListener; - if (listener != null) { - listener.onPhoneStateChanged(mContext, serviceState); - } - } - - private void onConnectivityChanged(final Context context, final Intent intent) { - final ConnectivityListener listener = mListener; - if (listener != null) { - listener.onConnectivityStateChanged(context, intent); - } - } - - public void register(final ConnectivityListener listener) { - Assert.isTrue(mListener == null || mListener == listener); - if (mListener == null) { - if (mTelephonyManager != null) { - mCurrentServiceState = (PhoneUtils.getDefault().isAirplaneModeOn() ? - ServiceState.STATE_POWER_OFF : ServiceState.STATE_IN_SERVICE); - mTelephonyManager.listen(mPhoneStateListener, - PhoneStateListener.LISTEN_SERVICE_STATE); - } - if (mConnMgr != null) { - mContext.registerReceiver(mReceiver, mIntentFilter); - } - } - mListener = listener; - } - - public void unregister() { - if (mListener != null) { - if (mTelephonyManager != null) { - mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); - mCurrentServiceState = ServiceState.STATE_POWER_OFF; - } - if (mConnMgr != null) { - mContext.unregisterReceiver(mReceiver); - } - } - mListener = null; - } - - /** - * Connectivity change broadcast receiver. This gets the network connectivity updates. - * In case we don't get the active connectivity when we first acquire the network, - * this receiver will notify us when it is connected, so to unblock the waiting thread - * which is sending the message. - */ - public class ConnectivityBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(final Context context, final Intent intent) { - if (!intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) { - return; - } - - onConnectivityChanged(context, intent); - } - } - - private int mSignalLevel = SIGNAL_STRENGTH_NONE_OR_UNKNOWN; - - // We use a separate instance than mPhoneStateListener because the lifetimes are different. - private final PhoneStateListener mSignalStrengthListener = new PhoneStateListener() { - @Override - public void onSignalStrengthsChanged(final SignalStrength signalStrength) { - mSignalLevel = getLevel(signalStrength); - } - }; - - public void registerForSignalStrength() { - mTelephonyManager.listen( - mSignalStrengthListener, PhoneStateListener.LISTEN_SIGNAL_STRENGTHS); - } - - public void unregisterForSignalStrength() { - mTelephonyManager.listen(mSignalStrengthListener, PhoneStateListener.LISTEN_NONE); - } - - /** - * @param subId This is ignored because TelephonyManager does not support it. - * @return Signal strength as level 0..4 - */ - public int getSignalLevel(final int subId) { - return mSignalLevel; - } - - private static final int SIGNAL_STRENGTH_NONE_OR_UNKNOWN = 0; - private static final int SIGNAL_STRENGTH_POOR = 1; - private static final int SIGNAL_STRENGTH_MODERATE = 2; - private static final int SIGNAL_STRENGTH_GOOD = 3; - private static final int SIGNAL_STRENGTH_GREAT = 4; - - private static final int GSM_SIGNAL_STRENGTH_GREAT = 12; - private static final int GSM_SIGNAL_STRENGTH_GOOD = 8; - private static final int GSM_SIGNAL_STRENGTH_MODERATE = 8; - - private static int getLevel(final SignalStrength signalStrength) { - if (signalStrength.isGsm()) { - // From frameworks/base/telephony/java/android/telephony/CellSignalStrengthGsm.java - - // ASU ranges from 0 to 31 - TS 27.007 Sec 8.5 - // asu = 0 (-113dB or less) is very weak - // signal, its better to show 0 bars to the user in such cases. - // asu = 99 is a special case, where the signal strength is unknown. - final int asu = signalStrength.getGsmSignalStrength(); - if (asu <= 2 || asu == 99) return SIGNAL_STRENGTH_NONE_OR_UNKNOWN; - else if (asu >= GSM_SIGNAL_STRENGTH_GREAT) return SIGNAL_STRENGTH_GREAT; - else if (asu >= GSM_SIGNAL_STRENGTH_GOOD) return SIGNAL_STRENGTH_GOOD; - else if (asu >= GSM_SIGNAL_STRENGTH_MODERATE) return SIGNAL_STRENGTH_MODERATE; - else return SIGNAL_STRENGTH_POOR; - } else { - // From frameworks/base/telephony/java/android/telephony/CellSignalStrengthCdma.java - - final int cdmaLevel = getCdmaLevel(signalStrength); - final int evdoLevel = getEvdoLevel(signalStrength); - if (evdoLevel == SIGNAL_STRENGTH_NONE_OR_UNKNOWN) { - /* We don't know evdo, use cdma */ - return getCdmaLevel(signalStrength); - } else if (cdmaLevel == SIGNAL_STRENGTH_NONE_OR_UNKNOWN) { - /* We don't know cdma, use evdo */ - return getEvdoLevel(signalStrength); - } else { - /* We know both, use the lowest level */ - return cdmaLevel < evdoLevel ? cdmaLevel : evdoLevel; - } - } - } - - /** - * Get cdma as level 0..4 - */ - private static int getCdmaLevel(final SignalStrength signalStrength) { - final int cdmaDbm = signalStrength.getCdmaDbm(); - final int cdmaEcio = signalStrength.getCdmaEcio(); - int levelDbm; - int levelEcio; - if (cdmaDbm >= -75) levelDbm = SIGNAL_STRENGTH_GREAT; - else if (cdmaDbm >= -85) levelDbm = SIGNAL_STRENGTH_GOOD; - else if (cdmaDbm >= -95) levelDbm = SIGNAL_STRENGTH_MODERATE; - else if (cdmaDbm >= -100) levelDbm = SIGNAL_STRENGTH_POOR; - else levelDbm = SIGNAL_STRENGTH_NONE_OR_UNKNOWN; - // Ec/Io are in dB*10 - if (cdmaEcio >= -90) levelEcio = SIGNAL_STRENGTH_GREAT; - else if (cdmaEcio >= -110) levelEcio = SIGNAL_STRENGTH_GOOD; - else if (cdmaEcio >= -130) levelEcio = SIGNAL_STRENGTH_MODERATE; - else if (cdmaEcio >= -150) levelEcio = SIGNAL_STRENGTH_POOR; - else levelEcio = SIGNAL_STRENGTH_NONE_OR_UNKNOWN; - final int level = (levelDbm < levelEcio) ? levelDbm : levelEcio; - return level; - } - /** - * Get Evdo as level 0..4 - */ - private static int getEvdoLevel(final SignalStrength signalStrength) { - final int evdoDbm = signalStrength.getEvdoDbm(); - final int evdoSnr = signalStrength.getEvdoSnr(); - int levelEvdoDbm; - int levelEvdoSnr; - if (evdoDbm >= -65) levelEvdoDbm = SIGNAL_STRENGTH_GREAT; - else if (evdoDbm >= -75) levelEvdoDbm = SIGNAL_STRENGTH_GOOD; - else if (evdoDbm >= -90) levelEvdoDbm = SIGNAL_STRENGTH_MODERATE; - else if (evdoDbm >= -105) levelEvdoDbm = SIGNAL_STRENGTH_POOR; - else levelEvdoDbm = SIGNAL_STRENGTH_NONE_OR_UNKNOWN; - if (evdoSnr >= 7) levelEvdoSnr = SIGNAL_STRENGTH_GREAT; - else if (evdoSnr >= 5) levelEvdoSnr = SIGNAL_STRENGTH_GOOD; - else if (evdoSnr >= 3) levelEvdoSnr = SIGNAL_STRENGTH_MODERATE; - else if (evdoSnr >= 1) levelEvdoSnr = SIGNAL_STRENGTH_POOR; - else levelEvdoSnr = SIGNAL_STRENGTH_NONE_OR_UNKNOWN; - final int level = (levelEvdoDbm < levelEvdoSnr) ? levelEvdoDbm : levelEvdoSnr; - return level; - } -} diff --git a/src/com/android/messaging/util/ContactRecipientEntryUtils.java b/src/com/android/messaging/util/ContactRecipientEntryUtils.java deleted file mode 100644 index 78c6ffd..0000000 --- a/src/com/android/messaging/util/ContactRecipientEntryUtils.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.net.Uri; -import android.provider.ContactsContract.DisplayNameSources; -import android.text.TextUtils; - -import com.android.ex.chips.RecipientEntry; -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.datamodel.BugleRecipientEntry; -import com.android.messaging.datamodel.data.ParticipantData; - -/** - * Provides utility methods around creating RecipientEntry instance specific to Bugle's needs. - */ -public class ContactRecipientEntryUtils { - /** - * A special contact id for generated contacts with no display name (number only) and avatar. - * By default, the chips UI doesn't load any avatar for chips with no display name, or where - * the display name is the same as phone number (which is true for unknown contacts). - * Since Bugle always generate a default avatar for all contacts, this is used to replace - * those default generated chips with a phone number and no avatars. - */ - private static final long CONTACT_ID_NUMBER_WITH_AVATAR = -1000; - - /** - * A generated special contact which says "Send to xxx" in the contact list, which allows - * a user to direct send an SMS to a number that was manually typed in. - */ - private static final long CONTACT_ID_SENDTO_DESTINATION = -1001; - - /** - * Construct a special "Send to xxx" entry for a given destination. - */ - public static RecipientEntry constructSendToDestinationEntry(final String destination) { - return constructSpecialRecipientEntry(destination, CONTACT_ID_SENDTO_DESTINATION); - } - - /** - * Construct a generated contact entry but with rendered avatar. - */ - public static RecipientEntry constructNumberWithAvatarEntry(final String destination) { - return constructSpecialRecipientEntry(destination, CONTACT_ID_NUMBER_WITH_AVATAR); - } - - private static RecipientEntry constructSpecialRecipientEntry(final String destination, - final long contactId) { - // For the send-to-destination (e.g. "Send to xxx" in the auto-complete drop-down) - // we want to show a default avatar with a static background so that it doesn't flicker - // as the user types. - final Uri avatarUri = contactId == CONTACT_ID_SENDTO_DESTINATION ? - AvatarUriUtil.DEFAULT_BACKGROUND_AVATAR : null; - return BugleRecipientEntry.constructTopLevelEntry(null, DisplayNameSources.STRUCTURED_NAME, - destination, RecipientEntry.INVALID_DESTINATION_TYPE, null, contactId, - null, contactId, avatarUri, true, null); - } - - /** - * Gets the display name for contact list only. For most cases this is the same as the normal - * contact name, but there are cases where these two differ. For example, for the - * send to typed number item, we'd like to show "Send to xxx" in the contact list. However, - * when this item is actually added to the chips edit box, we would like to show just the - * phone number (i.e. no display name). - */ - public static String getDisplayNameForContactList(final RecipientEntry entry) { - if (entry.getContactId() == CONTACT_ID_SENDTO_DESTINATION) { - return Factory.get().getApplicationContext().getResources().getString( - R.string.contact_list_send_to_text, formatDestination(entry)); - } else if (!TextUtils.isEmpty(entry.getDisplayName())) { - return entry.getDisplayName(); - } else { - return formatDestination(entry); - } - } - - public static String formatDestination(final RecipientEntry entry) { - return PhoneUtils.getDefault().formatForDisplay(entry.getDestination()); - } - - /** - * Returns true if the given entry has only avatar and number - */ - public static boolean isAvatarAndNumberOnlyContact(final RecipientEntry entry) { - return entry.getContactId() == CONTACT_ID_NUMBER_WITH_AVATAR; - } - - /** - * Returns true if the given entry is a special send to number item. - */ - public static boolean isSendToDestinationContact(final RecipientEntry entry) { - return entry.getContactId() == CONTACT_ID_SENDTO_DESTINATION; - } - - /** - * Returns true if the given participant is a special send to number item. - */ - public static boolean isSendToDestinationContact(final ParticipantData participant) { - return participant.getContactId() == CONTACT_ID_SENDTO_DESTINATION; - } -} diff --git a/src/com/android/messaging/util/ContactUtil.java b/src/com/android/messaging/util/ContactUtil.java deleted file mode 100644 index 8555889..0000000 --- a/src/com/android/messaging/util/ContactUtil.java +++ /dev/null @@ -1,525 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.Manifest; -import android.content.Context; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.net.Uri; -import android.provider.ContactsContract; -import android.provider.ContactsContract.CommonDataKinds.Email; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.CommonDataKinds.StructuredName; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Directory; -import android.provider.ContactsContract.DisplayNameSources; -import android.provider.ContactsContract.PhoneLookup; -import android.provider.ContactsContract.Profile; -import android.text.TextUtils; -import android.view.View; - -import com.android.ex.chips.RecipientEntry; -import com.android.messaging.Factory; -import com.android.messaging.datamodel.CursorQueryData; -import com.android.messaging.datamodel.FrequentContactsCursorQueryData; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.sms.MmsSmsUtils; -import com.android.messaging.ui.contact.AddContactsConfirmationDialog; -import com.google.common.annotations.VisibleForTesting; - -/** - * Utility class including logic to list, filter, and lookup phone and emails in CP2. - */ -@VisibleForTesting -public class ContactUtil { - - /** - * Index of different columns in phone or email queries. All queries below should confirm to - * this column content and ordering so that caller can use the uniformed way to process - * returned cursors. - */ - public static final int INDEX_CONTACT_ID = 0; - public static final int INDEX_DISPLAY_NAME = 1; - public static final int INDEX_PHOTO_URI = 2; - public static final int INDEX_PHONE_EMAIL = 3; - public static final int INDEX_PHONE_EMAIL_TYPE = 4; - public static final int INDEX_PHONE_EMAIL_LABEL = 5; - - // An optional lookup_id column used by PhoneLookupQuery that is needed when querying for - // contact information. - public static final int INDEX_LOOKUP_KEY = 6; - - // An optional _id column to query results that need to be displayed in a list view. - public static final int INDEX_DATA_ID = 7; - - // An optional sort_key column for displaying contact section labels. - public static final int INDEX_SORT_KEY = 8; - - // Lookup key column index specific to frequent contacts query. - public static final int INDEX_LOOKUP_KEY_FREQUENT = 3; - - /** - * Constants for listing and filtering phones. - */ - public static class PhoneQuery { - public static final String SORT_KEY = Phone.SORT_KEY_PRIMARY; - - public static final String[] PROJECTION = new String[] { - Phone.CONTACT_ID, // 0 - Phone.DISPLAY_NAME_PRIMARY, // 1 - Phone.PHOTO_THUMBNAIL_URI, // 2 - Phone.NUMBER, // 3 - Phone.TYPE, // 4 - Phone.LABEL, // 5 - Phone.LOOKUP_KEY, // 6 - Phone._ID, // 7 - PhoneQuery.SORT_KEY, // 8 - }; - } - - /** - * Constants for looking up phone numbers. - */ - public static class PhoneLookupQuery { - public static final String[] PROJECTION = new String[] { - // The _ID field points to the contact id of the content - PhoneLookup._ID, // 0 - PhoneLookup.DISPLAY_NAME, // 1 - PhoneLookup.PHOTO_THUMBNAIL_URI, // 2 - PhoneLookup.NUMBER, // 3 - PhoneLookup.TYPE, // 4 - PhoneLookup.LABEL, // 5 - PhoneLookup.LOOKUP_KEY, // 6 - // The data id is not included as part of the projection since it's not part of - // PhoneLookup. This is okay because the _id field serves as both the data id and - // contact id. Also we never show the results directly in a list view so we are not - // concerned about duplicated _id's (namely, the same contact has two same phone - // numbers) - }; - } - - public static class FrequentContactQuery { - public static final String[] PROJECTION = new String[] { - Contacts._ID, // 0 - Contacts.DISPLAY_NAME, // 1 - Contacts.PHOTO_URI, // 2 - Phone.LOOKUP_KEY, // 3 - }; - } - - /** - * Constants for listing and filtering emails. - */ - public static class EmailQuery { - public static final String SORT_KEY = Email.SORT_KEY_PRIMARY; - - public static final String[] PROJECTION = new String[] { - Email.CONTACT_ID, // 0 - Email.DISPLAY_NAME_PRIMARY, // 1 - Email.PHOTO_THUMBNAIL_URI, // 2 - Email.ADDRESS, // 3 - Email.TYPE, // 4 - Email.LABEL, // 5 - Email.LOOKUP_KEY, // 6 - Email._ID, // 7 - EmailQuery.SORT_KEY, // 8 - }; - } - - public static final int INDEX_SELF_QUERY_LOOKUP_KEY = 3; - - /** - * Constants for querying self from CP2. - */ - public static class SelfQuery { - public static final String[] PROJECTION = new String[] { - Profile._ID, // 0 - Profile.DISPLAY_NAME_PRIMARY, // 1 - Profile.PHOTO_THUMBNAIL_URI, // 2 - Profile.LOOKUP_KEY // 3 - // Phone number, type, label and data_id is not provided in this projection since - // Profile CONTENT_URI doesn't include this information. Also, we don't need it - // we just need the name and avatar url. - }; - } - - public static class StructuredNameQuery { - public static final String[] PROJECTION = new String[] { - StructuredName.DISPLAY_NAME, - StructuredName.GIVEN_NAME, - StructuredName.FAMILY_NAME, - StructuredName.PREFIX, - StructuredName.MIDDLE_NAME, - StructuredName.SUFFIX - }; - } - - public static final int INDEX_STRUCTURED_NAME_DISPLAY_NAME = 0; - public static final int INDEX_STRUCTURED_NAME_GIVEN_NAME = 1; - public static final int INDEX_STRUCTURED_NAME_FAMILY_NAME = 2; - public static final int INDEX_STRUCTURED_NAME_PREFIX = 3; - public static final int INDEX_STRUCTURED_NAME_MIDDLE_NAME = 4; - public static final int INDEX_STRUCTURED_NAME_SUFFIX = 5; - - public static final long INVALID_CONTACT_ID = -1; - - /** - * This class is static. No need to create an instance. - */ - private ContactUtil() { - } - - /** - * Shows a contact card or add to contacts dialog for the given contact info - * @param view The view whose click triggered this to show - * @param contactId The id of the contact in the android contacts DB - * @param contactLookupKey The lookup key from contacts DB - * @param avatarUri Uri to the avatar image if available - * @param normalizedDestination The normalized phone number or email - */ - public static void showOrAddContact(final View view, final long contactId, - final String contactLookupKey, final Uri avatarUri, - final String normalizedDestination) { - if (contactId > ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED - && !TextUtils.isEmpty(contactLookupKey)) { - final Uri lookupUri = - ContactsContract.Contacts.getLookupUri(contactId, contactLookupKey); - ContactsContract.QuickContact.showQuickContact(view.getContext(), view, lookupUri, - ContactsContract.QuickContact.MODE_LARGE, null); - } else if (!TextUtils.isEmpty(normalizedDestination) && !TextUtils.equals( - normalizedDestination, ParticipantData.getUnknownSenderDestination())) { - final AddContactsConfirmationDialog dialog = new AddContactsConfirmationDialog( - view.getContext(), avatarUri, normalizedDestination); - dialog.show(); - } - } - - @VisibleForTesting - public static CursorQueryData getSelf(final Context context) { - if (!ContactUtil.hasReadContactsPermission()) { - return CursorQueryData.getEmptyQueryData(); - } - return new CursorQueryData(context, Profile.CONTENT_URI, SelfQuery.PROJECTION, null, null, - null); - } - - /** - * Get a list of phones sorted by contact name. One contact may have multiple phones. - * In that case, each phone will be returned as a separate record in the result cursor. - */ - @VisibleForTesting - public static CursorQueryData getPhones(final Context context) { - if (!ContactUtil.hasReadContactsPermission()) { - return CursorQueryData.getEmptyQueryData(); - } - - // The AOSP Contacts provider allows adding a ContactsContract.REMOVE_DUPLICATE_ENTRIES - // query parameter that removes duplicate (raw) numbers. Unfortunately, we can't use that - // because it causes the some phones' contacts provider to return incorrect sections. - final Uri uri = Phone.CONTENT_URI.buildUpon().appendQueryParameter( - ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)) - .appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true") - .build(); - - return new CursorQueryData(context, uri, PhoneQuery.PROJECTION, null, null, - PhoneQuery.SORT_KEY); - } - - /** - * Lookup a destination (phone, email). Supplied destination should be a relatively complete - * one for this to succeed. PhoneLookup / EmailLookup URI will apply some smartness to do a - * loose match to see whether there is a contact that matches this destination. - */ - public static CursorQueryData lookupDestination(final Context context, - final String destination) { - if (MmsSmsUtils.isEmailAddress(destination)) { - return ContactUtil.lookupEmail(context, destination); - } else { - return ContactUtil.lookupPhone(context, destination); - } - } - - /** - * Returns whether the search text indicates an email based search or a phone number based one. - */ - private static boolean shouldFilterForEmail(final String searchText) { - return searchText != null && searchText.contains("@"); - } - - /** - * Get a list of destinations (phone, email) matching the partial destination. - */ - public static CursorQueryData filterDestination(final Context context, - final String destination) { - if (shouldFilterForEmail(destination)) { - return ContactUtil.filterEmails(context, destination); - } else { - return ContactUtil.filterPhones(context, destination); - } - } - - /** - * Get a list of phones matching a search criteria. The search may be on contact name or - * phone number. In case search is on contact name, all matching contact's phone number - * will be returned. - * NOTE: This is visible for testing only, clients should only call filterDestination() since - * we support email addresses as well. - */ - @VisibleForTesting - public static CursorQueryData filterPhones(final Context context, final String query) { - if (!ContactUtil.hasReadContactsPermission()) { - return CursorQueryData.getEmptyQueryData(); - } - - final Uri uri = Phone.CONTENT_FILTER_URI.buildUpon() - .appendPath(query).appendQueryParameter( - ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)) - .build(); - - return new CursorQueryData(context, uri, PhoneQuery.PROJECTION, null, null, - PhoneQuery.SORT_KEY); - } - - /** - * Lookup a phone based on a phone number. Supplied phone should be a relatively complete - * phone number for this to succeed. PhoneLookup URI will apply some smartness to do a - * loose match to see whether there is a contact that matches this phone. - * NOTE: This is visible for testing only, clients should only call lookupDestination() since - * we support email addresses as well. - */ - @VisibleForTesting - public static CursorQueryData lookupPhone(final Context context, final String phone) { - if (!ContactUtil.hasReadContactsPermission()) { - return CursorQueryData.getEmptyQueryData(); - } - - final Uri uri = getPhoneLookupUri().buildUpon() - .appendPath(phone).build(); - - return new CursorQueryData(context, uri, PhoneLookupQuery.PROJECTION, null, null, null); - } - - /** - * Get frequently contacted people. This queries for Contacts.CONTENT_STREQUENT_URI, which - * includes both starred or frequently contacted people. - */ - public static CursorQueryData getFrequentContacts(final Context context) { - if (!ContactUtil.hasReadContactsPermission()) { - return CursorQueryData.getEmptyQueryData(); - } - - return new FrequentContactsCursorQueryData(context, FrequentContactQuery.PROJECTION, - null, null, null); - } - - /** - * Get a list of emails matching a search criteria. In Bugle, since email is not a common - * usage scenario, we should only do email search after user typed in a query indicating - * an intention to search by email (for example, "joe@"). - * NOTE: This is visible for testing only, clients should only call filterDestination() since - * we support email addresses as well. - */ - @VisibleForTesting - public static CursorQueryData filterEmails(final Context context, final String query) { - if (!ContactUtil.hasReadContactsPermission()) { - return CursorQueryData.getEmptyQueryData(); - } - - final Uri uri = Email.CONTENT_FILTER_URI.buildUpon() - .appendPath(query).appendQueryParameter( - ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)) - .build(); - - return new CursorQueryData(context, uri, EmailQuery.PROJECTION, null, null, - EmailQuery.SORT_KEY); - } - - /** - * Lookup emails based a complete email address. Since there is no special logic needed for - * email lookup, this simply calls filterEmails. - * NOTE: This is visible for testing only, clients should only call lookupDestination() since - * we support email addresses as well. - */ - @VisibleForTesting - public static CursorQueryData lookupEmail(final Context context, final String email) { - if (!ContactUtil.hasReadContactsPermission()) { - return CursorQueryData.getEmptyQueryData(); - } - - final Uri uri = getEmailContentLookupUri().buildUpon() - .appendPath(email).appendQueryParameter( - ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)) - .build(); - - return new CursorQueryData(context, uri, EmailQuery.PROJECTION, null, null, - EmailQuery.SORT_KEY); - } - - /** - * Looks up the structured name for a contact. - * - * @param primaryOnly If there are multiple raw contacts, set this flag to return only the - * name used as the primary display name. Otherwise, this method returns all names. - */ - private static CursorQueryData lookupStructuredName(final Context context, final long contactId, - final boolean primaryOnly) { - if (!ContactUtil.hasReadContactsPermission()) { - return CursorQueryData.getEmptyQueryData(); - } - - // TODO: Handle enterprise contacts - final Uri uri = ContactsContract.Contacts.CONTENT_URI.buildUpon() - .appendPath(String.valueOf(contactId)) - .appendPath(ContactsContract.Contacts.Data.CONTENT_DIRECTORY).build(); - - String selection = ContactsContract.Data.MIMETYPE + "=?"; - final String[] selectionArgs = { - StructuredName.CONTENT_ITEM_TYPE - }; - if (primaryOnly) { - selection += " AND " + Contacts.DISPLAY_NAME_PRIMARY + "=" - + StructuredName.DISPLAY_NAME; - } - - return new CursorQueryData(context, uri, - StructuredNameQuery.PROJECTION, selection, selectionArgs, null); - } - - /** - * Looks up the first name for a contact. If there are multiple raw - * contacts, this returns the name that is associated with the contact's - * primary display name. The name is null when contact id does not exist - * (possibly because it is a corp contact) or it does not have a first name. - */ - public static String lookupFirstName(final Context context, final long contactId) { - if (isEnterpriseContactId(contactId)) { - return null; - } - String firstName = null; - Cursor nameCursor = null; - try { - nameCursor = ContactUtil.lookupStructuredName(context, contactId, true) - .performSynchronousQuery(); - if (nameCursor != null && nameCursor.moveToFirst()) { - firstName = nameCursor.getString(ContactUtil.INDEX_STRUCTURED_NAME_GIVEN_NAME); - } - } finally { - if (nameCursor != null) { - nameCursor.close(); - } - } - return firstName; - } - - /** - * Creates a RecipientEntry from the provided data fields (from the contacts cursor). - * @param firstLevel whether this item is the first entry of this contact in the list. - */ - public static RecipientEntry createRecipientEntry(final String displayName, - final int displayNameSource, final String destination, final int destinationType, - final String destinationLabel, final long contactId, final String lookupKey, - final long dataId, final String photoThumbnailUri, final boolean firstLevel) { - if (firstLevel) { - return RecipientEntry.constructTopLevelEntry(displayName, displayNameSource, - destination, destinationType, destinationLabel, contactId, null, dataId, - photoThumbnailUri, true, lookupKey); - } else { - return RecipientEntry.constructSecondLevelEntry(displayName, displayNameSource, - destination, destinationType, destinationLabel, contactId, null, dataId, - photoThumbnailUri, true, lookupKey); - } - } - - /** - * Creates a RecipientEntry for PhoneQuery result. The result is then displayed in the - * contact search drop down or as replacement chips in the chips edit box. - */ - public static RecipientEntry createRecipientEntryForPhoneQuery(final Cursor cursor, - final boolean isFirstLevel) { - final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID); - final String displayName = cursor.getString( - ContactUtil.INDEX_DISPLAY_NAME); - final String photoThumbnailUri = cursor.getString( - ContactUtil.INDEX_PHOTO_URI); - final String destination = cursor.getString( - ContactUtil.INDEX_PHONE_EMAIL); - final int destinationType = cursor.getInt( - ContactUtil.INDEX_PHONE_EMAIL_TYPE); - final String destinationLabel = cursor.getString( - ContactUtil.INDEX_PHONE_EMAIL_LABEL); - final String lookupKey = cursor.getString( - ContactUtil.INDEX_LOOKUP_KEY); - - // PhoneQuery uses the contact id as the data id ("_id"). - final long dataId = contactId; - - return createRecipientEntry(displayName, - DisplayNameSources.STRUCTURED_NAME, destination, destinationType, - destinationLabel, contactId, lookupKey, dataId, photoThumbnailUri, - isFirstLevel); - } - - /** - * Returns if a given contact id is valid. - */ - public static boolean isValidContactId(final long contactId) { - return contactId >= 0; - } - - /** - * Returns if a given contact id belongs to managed profile. - */ - public static boolean isEnterpriseContactId(final long contactId) { - return isWorkProfileSupported() - && ContactsContract.Contacts.isEnterpriseContactId(contactId); - } - - /** - * Returns if managed profile is supported. - */ - public static boolean isWorkProfileSupported() { - final PackageManager pm = Factory.get().getApplicationContext().getPackageManager(); - return pm.hasSystemFeature(PackageManager.FEATURE_MANAGED_USERS); - } - - /** - * Returns Email lookup uri that will query both primary and corp profile - */ - private static Uri getEmailContentLookupUri() { - if (isWorkProfileSupported() && OsUtil.isAtLeastM()) { - // TODO: use Email.ENTERPRISE_CONTENT_LOOKUP_URI, which will be available in M SDK API - return Uri.parse("content://com.android.contacts/data/emails/lookup_enterprise"); - } - return Email.CONTENT_LOOKUP_URI; - } - - /** - * Returns PhoneLookup URI. - */ - public static Uri getPhoneLookupUri() { - // Apply it to M only - if (isWorkProfileSupported() && OsUtil.isAtLeastM()) { - return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI; - } - return PhoneLookup.CONTENT_FILTER_URI; - } - - public static boolean hasReadContactsPermission() { - return OsUtil.hasPermission(Manifest.permission.READ_CONTACTS); - } -} diff --git a/src/com/android/messaging/util/ContentType.java b/src/com/android/messaging/util/ContentType.java deleted file mode 100644 index bb4a7b2..0000000 --- a/src/com/android/messaging/util/ContentType.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright (C) 2007-2008 Esmertec AG. - * Copyright (C) 2007-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 com.android.messaging.util; - -import android.webkit.MimeTypeMap; - -public final class ContentType { - public static String THREE_GPP_EXTENSION = "3gp"; - public static String VIDEO_MP4_EXTENSION = "mp4"; - // Default extension used when we don't know one. - public static String DEFAULT_EXTENSION = "dat"; - - public static final int TYPE_IMAGE = 0; - public static final int TYPE_VIDEO = 1; - public static final int TYPE_AUDIO = 2; - public static final int TYPE_VCARD = 3; - public static final int TYPE_OTHER = 4; - - public static final String ANY_TYPE = "*/*"; - public static final String MMS_MESSAGE = "application/vnd.wap.mms-message"; - // The phony content type for generic PDUs (e.g. ReadOrig.ind, - // Notification.ind, Delivery.ind). - public static final String MMS_GENERIC = "application/vnd.wap.mms-generic"; - public static final String MMS_MULTIPART_MIXED = "application/vnd.wap.multipart.mixed"; - public static final String MMS_MULTIPART_RELATED = "application/vnd.wap.multipart.related"; - public static final String MMS_MULTIPART_ALTERNATIVE = - "application/vnd.wap.multipart.alternative"; - - public static final String TEXT_PLAIN = "text/plain"; - public static final String TEXT_HTML = "text/html"; - public static final String TEXT_VCALENDAR = "text/x-vCalendar"; - public static final String TEXT_VCARD = "text/x-vCard"; - - public static final String IMAGE_PREFIX = "image/"; - public static final String IMAGE_UNSPECIFIED = "image/*"; - public static final String IMAGE_JPEG = "image/jpeg"; - public static final String IMAGE_JPG = "image/jpg"; - public static final String IMAGE_GIF = "image/gif"; - public static final String IMAGE_WBMP = "image/vnd.wap.wbmp"; - public static final String IMAGE_PNG = "image/png"; - public static final String IMAGE_X_MS_BMP = "image/x-ms-bmp"; - - public static final String AUDIO_UNSPECIFIED = "audio/*"; - public static final String AUDIO_AAC = "audio/aac"; - public static final String AUDIO_AMR = "audio/amr"; - public static final String AUDIO_IMELODY = "audio/imelody"; - public static final String AUDIO_MID = "audio/mid"; - public static final String AUDIO_MIDI = "audio/midi"; - public static final String AUDIO_MP3 = "audio/mp3"; - public static final String AUDIO_MPEG3 = "audio/mpeg3"; - public static final String AUDIO_MPEG = "audio/mpeg"; - public static final String AUDIO_MPG = "audio/mpg"; - public static final String AUDIO_MP4 = "audio/mp4"; - public static final String AUDIO_MP4_LATM = "audio/mp4-latm"; - public static final String AUDIO_X_MID = "audio/x-mid"; - public static final String AUDIO_X_MIDI = "audio/x-midi"; - public static final String AUDIO_X_MP3 = "audio/x-mp3"; - public static final String AUDIO_X_MPEG3 = "audio/x-mpeg3"; - public static final String AUDIO_X_MPEG = "audio/x-mpeg"; - public static final String AUDIO_X_MPG = "audio/x-mpg"; - public static final String AUDIO_3GPP = "audio/3gpp"; - public static final String AUDIO_X_WAV = "audio/x-wav"; - public static final String AUDIO_OGG = "application/ogg"; - - public static final String MULTIPART_MIXED = "multipart/mixed"; - - public static final String VIDEO_UNSPECIFIED = "video/*"; - public static final String VIDEO_3GP = "video/3gp"; - public static final String VIDEO_3GPP = "video/3gpp"; - public static final String VIDEO_3G2 = "video/3gpp2"; - public static final String VIDEO_H263 = "video/h263"; - public static final String VIDEO_M4V = "video/m4v"; - public static final String VIDEO_MP4 = "video/mp4"; - public static final String VIDEO_MPEG = "video/mpeg"; - public static final String VIDEO_MPEG4 = "video/mpeg4"; - public static final String VIDEO_WEBM = "video/webm"; - - public static final String APP_SMIL = "application/smil"; - public static final String APP_WAP_XHTML = "application/vnd.wap.xhtml+xml"; - public static final String APP_XHTML = "application/xhtml+xml"; - - public static final String APP_DRM_CONTENT = "application/vnd.oma.drm.content"; - public static final String APP_DRM_MESSAGE = "application/vnd.oma.drm.message"; - - // This class should never be instantiated. - private ContentType() { - } - - public static boolean isTextType(final String contentType) { - return TEXT_PLAIN.equals(contentType) - || TEXT_HTML.equals(contentType) - || APP_WAP_XHTML.equals(contentType); - } - - public static boolean isMediaType(final String contentType) { - return isImageType(contentType) - || isVideoType(contentType) - || isAudioType(contentType) - || isVCardType(contentType); - } - - public static boolean isImageType(final String contentType) { - return (null != contentType) && contentType.startsWith(IMAGE_PREFIX); - } - - public static boolean isAudioType(final String contentType) { - return (null != contentType) && - (contentType.startsWith("audio/") || contentType.equalsIgnoreCase(AUDIO_OGG)); - } - - public static boolean isVideoType(final String contentType) { - return (null != contentType) && contentType.startsWith("video/"); - } - - public static boolean isVCardType(final String contentType) { - return (null != contentType) && contentType.equalsIgnoreCase(TEXT_VCARD); - } - - public static boolean isDrmType(final String contentType) { - return (null != contentType) - && (contentType.equals(APP_DRM_CONTENT) - || contentType.equals(APP_DRM_MESSAGE)); - } - - public static boolean isUnspecified(final String contentType) { - return (null != contentType) && contentType.endsWith("*"); - } - - /** - * If the content type is a type which can be displayed in the conversation list as a preview. - */ - public static boolean isConversationListPreviewableType(final String contentType) { - return ContentType.isAudioType(contentType) || ContentType.isVideoType(contentType) || - ContentType.isImageType(contentType) || ContentType.isVCardType(contentType); - } - - /** - * Given a filename, look at the extension and try and determine the mime type. - * - * @param fileName a filename to determine the type from, such as img1231.jpg - * @param contentTypeDefault type to use when the content type can't be determined from the file - * extension. It can be null or a type such as ContentType.IMAGE_UNSPECIFIED - * @return Content type of the extension. - */ - public static String getContentTypeFromExtension(final String fileName, - final String contentTypeDefault) { - final MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); - final String extension = MimeTypeMap.getFileExtensionFromUrl(fileName); - String contentType = mimeTypeMap.getMimeTypeFromExtension(extension); - if (contentType == null) { - contentType = contentTypeDefault; - } - return contentType; - } - - /** - * Get the common file extension for a given content type - * @param contentType The content type - * @return The extension without the . - */ - public static String getExtension(final String contentType) { - if (VIDEO_MP4.equals(contentType)) { - return VIDEO_MP4_EXTENSION; - } else if (VIDEO_3GPP.equals(contentType)) { - return THREE_GPP_EXTENSION; - } else { - return DEFAULT_EXTENSION; - } - } -} diff --git a/src/com/android/messaging/util/ConversationIdSet.java b/src/com/android/messaging/util/ConversationIdSet.java deleted file mode 100644 index 75bf634..0000000 --- a/src/com/android/messaging/util/ConversationIdSet.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.android.messaging.util; - -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; - -/** - * Utility class to make it easy to store multiple conversation id strings in a single string - * with delimeters. - */ -public class ConversationIdSet extends HashSet<String> { - private static final String JOIN_DELIMITER = "|"; - private static final String SPLIT_DELIMITER = "\\|"; - - public ConversationIdSet() { - super(); - } - - public ConversationIdSet(final Collection<String> asList) { - super(asList); - } - - public String first() { - if (size() > 0) { - return iterator().next(); - } else { - return null; - } - } - - public static ConversationIdSet createSet(final String conversationIdSetString) { - ConversationIdSet set = null; - if (conversationIdSetString != null) { - set = new ConversationIdSet(Arrays.asList(conversationIdSetString.split( - SPLIT_DELIMITER))); - } - return set; - } - - public String getDelimitedString() { - return OsUtil.joinFromSetWithDelimiter(this, JOIN_DELIMITER); - } - - public static String join(final String conversationIdSet1, final String conversationIdSet2) { - String joined = null; - if (conversationIdSet1 == null) { - joined = conversationIdSet2; - } else if (conversationIdSet2 != null) { - joined = conversationIdSet1 + JOIN_DELIMITER + conversationIdSet2; - } - return joined; - } - -} diff --git a/src/com/android/messaging/util/CubicBezierInterpolator.java b/src/com/android/messaging/util/CubicBezierInterpolator.java deleted file mode 100644 index 317b3e6..0000000 --- a/src/com/android/messaging/util/CubicBezierInterpolator.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.view.animation.Interpolator; - -/** - * Class that acts as an interpolator to match the cubic-bezier css timing function where p0 is - * fixed at 0,0 and p3 is fixed at 1,1 - */ -public class CubicBezierInterpolator implements Interpolator { - private final float mX1; - private final float mY1; - private final float mX2; - private final float mY2; - - public CubicBezierInterpolator(final float x1, final float y1, final float x2, final float y2) { - mX1 = x1; - mY1 = y1; - mX2 = x2; - mY2 = y2; - } - - @Override - public float getInterpolation(float v) { - return getY(getTForXValue(v)); - } - - private float getX(final float t) { - return getCoordinate(t, mX1, mX2); - } - - private float getY(final float t) { - return getCoordinate(t, mY1, mY2); - } - - private float getCoordinate(float t, float p1, float p2) { - // Special case start and end. - if (t == 0.0f || t == 1.0f) { - return t; - } - - // Step one - from 4 points to 3. - float ip0 = linearInterpolate(0, p1, t); - float ip1 = linearInterpolate(p1, p2, t); - float ip2 = linearInterpolate(p2, 1, t); - - // Step two - from 3 points to 2. - ip0 = linearInterpolate(ip0, ip1, t); - ip1 = linearInterpolate(ip1, ip2, t); - - // Final step - last point. - return linearInterpolate(ip0, ip1, t); - } - - private float linearInterpolate(float a, float b, float progress) { - return a + (b - a) * progress; - } - - private float getTForXValue(final float x) { - final float epsilon = 1e-6f; - final int iterations = 8; - - if (x <= 0.0f) { - return 0.0f; - } else if (x >= 1.0f) { - return 1.0f; - } - - // Try gradient descent to solve for t. If it works, it is very fast. - float t = x; - float minT = 0.0f; - float maxT = 1.0f; - float value = 0.0f; - for (int i = 0; i < iterations; i++) { - value = getX(t); - double derivative = (getX(t + epsilon) - value) / epsilon; - if (Math.abs(value - x) < epsilon) { - return t; - } else if (Math.abs(derivative) < epsilon) { - break; - } else { - if (value < x) { - minT = t; - } else { - maxT = t; - } - t -= (value - x) / derivative; - } - } - - // If the gradient descent got stuck in a local minimum, e.g. because the - // derivative was close to 0, use an interval bisection instead. - for (int i = 0; Math.abs(value - x) > epsilon && i < iterations; i++) { - if (value < x) { - minT = t; - t = (t + maxT) / 2.0f; - } else { - maxT = t; - t = (t + minT) / 2.0f; - } - value = getX(t); - } - return t; - } -} diff --git a/src/com/android/messaging/util/Dates.java b/src/com/android/messaging/util/Dates.java deleted file mode 100644 index d012dfd..0000000 --- a/src/com/android/messaging/util/Dates.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; -import android.text.format.DateUtils; -import android.text.format.Time; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.google.common.annotations.VisibleForTesting; - -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -/** - * Collection of date utilities. - */ -public class Dates { - public static final long SECOND_IN_MILLIS = 1000; - public static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60; - public static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60; - public static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24; - public static final long WEEK_IN_MILLIS = DAY_IN_MILLIS * 7; - - // Flags to specify whether or not to use 12 or 24 hour mode. - // Callers of methods in this class should never have to specify these; this is really - // intended only for unit tests. - @SuppressWarnings("deprecation") - @VisibleForTesting public static final int FORCE_12_HOUR = DateUtils.FORMAT_12HOUR; - @SuppressWarnings("deprecation") - @VisibleForTesting public static final int FORCE_24_HOUR = DateUtils.FORMAT_24HOUR; - - /** - * Private default constructor - */ - private Dates() { - } - - private static Context getContext() { - return Factory.get().getApplicationContext(); - } - /** - * Get the relative time as a string - * - * @param time The time - * - * @return The relative time - */ - public static CharSequence getRelativeTimeSpanString(final long time) { - final long now = System.currentTimeMillis(); - if (now - time < DateUtils.MINUTE_IN_MILLIS) { - // Also fixes bug where posts appear in the future - return getContext().getResources().getText(R.string.posted_just_now); - } - - // Workaround for b/5657035. The platform method {@link DateUtils#getRelativeTimeSpan()} - // passes a null context to other platform methods. However, on some devices, this - // context is dereferenced when it shouldn't be and an NPE is thrown. We catch that - // here and use a slightly less precise time. - try { - return DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS, - DateUtils.FORMAT_ABBREV_RELATIVE).toString(); - } catch (final NullPointerException npe) { - return getShortRelativeTimeSpanString(time); - } - } - - public static CharSequence getConversationTimeString(final long time) { - return getTimeString(time, true /*abbreviated*/, false /*minPeriodToday*/); - } - - public static CharSequence getMessageTimeString(final long time) { - return getTimeString(time, false /*abbreviated*/, false /*minPeriodToday*/); - } - - public static CharSequence getWidgetTimeString(final long time, final boolean abbreviated) { - return getTimeString(time, abbreviated, true /*minPeriodToday*/); - } - - public static CharSequence getFastScrollPreviewTimeString(final long time) { - return getTimeString(time, true /* abbreviated */, true /* minPeriodToday */); - } - - public static CharSequence getMessageDetailsTimeString(final long time) { - final Context context = getContext(); - int flags; - if (android.text.format.DateFormat.is24HourFormat(context)) { - flags = FORCE_24_HOUR; - } else { - flags = FORCE_12_HOUR; - } - return getOlderThanAYearTimestamp(time, - context.getResources().getConfiguration().locale, false /*abbreviated*/, - flags); - } - - private static CharSequence getTimeString(final long time, final boolean abbreviated, - final boolean minPeriodToday) { - final Context context = getContext(); - int flags; - if (android.text.format.DateFormat.is24HourFormat(context)) { - flags = FORCE_24_HOUR; - } else { - flags = FORCE_12_HOUR; - } - return getTimestamp(time, System.currentTimeMillis(), abbreviated, - context.getResources().getConfiguration().locale, flags, minPeriodToday); - } - - @VisibleForTesting - public static CharSequence getTimestamp(final long time, final long now, - final boolean abbreviated, final Locale locale, final int flags, - final boolean minPeriodToday) { - final long timeDiff = now - time; - - if (!minPeriodToday && timeDiff < DateUtils.MINUTE_IN_MILLIS) { - return getLessThanAMinuteOldTimeString(abbreviated); - } else if (!minPeriodToday && timeDiff < DateUtils.HOUR_IN_MILLIS) { - return getLessThanAnHourOldTimeString(timeDiff, flags); - } else if (getNumberOfDaysPassed(time, now) == 0) { - return getTodayTimeStamp(time, flags); - } else if (timeDiff < DateUtils.WEEK_IN_MILLIS) { - return getThisWeekTimestamp(time, locale, abbreviated, flags); - } else if (timeDiff < DateUtils.YEAR_IN_MILLIS) { - return getThisYearTimestamp(time, locale, abbreviated, flags); - } else { - return getOlderThanAYearTimestamp(time, locale, abbreviated, flags); - } - } - - private static CharSequence getLessThanAMinuteOldTimeString( - final boolean abbreviated) { - return getContext().getResources().getText( - abbreviated ? R.string.posted_just_now : R.string.posted_now); - } - - private static CharSequence getLessThanAnHourOldTimeString(final long timeDiff, - final int flags) { - final long count = (timeDiff / MINUTE_IN_MILLIS); - final String format = getContext().getResources().getQuantityString( - R.plurals.num_minutes_ago, (int) count); - return String.format(format, count); - } - - private static CharSequence getTodayTimeStamp(final long time, final int flags) { - return DateUtils.formatDateTime(getContext(), time, - DateUtils.FORMAT_SHOW_TIME | flags); - } - - private static CharSequence getExplicitFormattedTime(final long time, final int flags, - final String format24, final String format12) { - SimpleDateFormat formatter; - if ((flags & FORCE_24_HOUR) == FORCE_24_HOUR) { - formatter = new SimpleDateFormat(format24); - } else { - formatter = new SimpleDateFormat(format12); - } - return formatter.format(new Date(time)); - } - - private static CharSequence getThisWeekTimestamp(final long time, - final Locale locale, final boolean abbreviated, final int flags) { - final Context context = getContext(); - if (abbreviated) { - return DateUtils.formatDateTime(context, time, - DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY | flags); - } else { - if (locale.equals(Locale.US)) { - return getExplicitFormattedTime(time, flags, "EEE HH:mm", "EEE h:mmaa"); - } else { - return DateUtils.formatDateTime(context, time, - DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_ABBREV_WEEKDAY - | flags); - } - } - } - - private static CharSequence getThisYearTimestamp(final long time, final Locale locale, - final boolean abbreviated, final int flags) { - final Context context = getContext(); - if (abbreviated) { - return DateUtils.formatDateTime(context, time, - DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH - | DateUtils.FORMAT_NO_YEAR | flags); - } else { - if (locale.equals(Locale.US)) { - return getExplicitFormattedTime(time, flags, "MMM d, HH:mm", "MMM d, h:mmaa"); - } else { - return DateUtils.formatDateTime(context, time, - DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_ABBREV_MONTH - | DateUtils.FORMAT_NO_YEAR - | flags); - } - } - } - - private static CharSequence getOlderThanAYearTimestamp(final long time, - final Locale locale, final boolean abbreviated, final int flags) { - final Context context = getContext(); - if (abbreviated) { - if (locale.equals(Locale.US)) { - return getExplicitFormattedTime(time, flags, "M/d/yy", "M/d/yy"); - } else { - return DateUtils.formatDateTime(context, time, - DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR - | DateUtils.FORMAT_NUMERIC_DATE); - } - } else { - if (locale.equals(Locale.US)) { - return getExplicitFormattedTime(time, flags, "M/d/yy, HH:mm", "M/d/yy, h:mmaa"); - } else { - return DateUtils.formatDateTime(context, time, - DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_YEAR - | flags); - } - } - } - - public static CharSequence getShortRelativeTimeSpanString(final long time) { - final long now = System.currentTimeMillis(); - final long duration = Math.abs(now - time); - - int resId; - long count; - - final Context context = getContext(); - - if (duration < HOUR_IN_MILLIS) { - count = duration / MINUTE_IN_MILLIS; - resId = R.plurals.num_minutes_ago; - } else if (duration < DAY_IN_MILLIS) { - count = duration / HOUR_IN_MILLIS; - resId = R.plurals.num_hours_ago; - } else if (duration < WEEK_IN_MILLIS) { - count = getNumberOfDaysPassed(time, now); - resId = R.plurals.num_days_ago; - } else { - // Although we won't be showing a time, there is a bug on some devices that use - // the passed in context. On these devices, passing in a {@code null} context - // here will generate an NPE. See b/5657035. - return DateUtils.formatDateRange(context, time, time, - DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_ABBREV_RELATIVE); - } - - final String format = context.getResources().getQuantityString(resId, (int) count); - return String.format(format, count); - } - - private static synchronized long getNumberOfDaysPassed(final long date1, final long date2) { - if (sThenTime == null) { - sThenTime = new Time(); - } - sThenTime.set(date1); - final int day1 = Time.getJulianDay(date1, sThenTime.gmtoff); - sThenTime.set(date2); - final int day2 = Time.getJulianDay(date2, sThenTime.gmtoff); - return Math.abs(day2 - day1); - } - - private static Time sThenTime; -} diff --git a/src/com/android/messaging/util/DebugUtils.java b/src/com/android/messaging/util/DebugUtils.java deleted file mode 100644 index f2c1d65..0000000 --- a/src/com/android/messaging/util/DebugUtils.java +++ /dev/null @@ -1,425 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.FragmentManager; -import android.app.FragmentTransaction; -import android.content.Context; -import android.content.DialogInterface; -import android.media.MediaPlayer; -import android.os.Environment; -import android.telephony.SmsMessage; -import android.text.TextUtils; -import android.widget.ArrayAdapter; - -import com.android.messaging.R; -import com.android.messaging.datamodel.SyncManager; -import com.android.messaging.datamodel.action.DumpDatabaseAction; -import com.android.messaging.datamodel.action.LogTelephonyDatabaseAction; -import com.android.messaging.sms.MmsUtils; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.ui.debug.DebugSmsMmsFromDumpFileDialogFragment; -import com.google.common.io.ByteStreams; - -import java.io.BufferedInputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FilenameFilter; -import java.io.IOException; -import java.io.StreamCorruptedException; - -public class DebugUtils { - private static final String TAG = "bugle.util.DebugUtils"; - - private static boolean sDebugNoise; - private static boolean sDebugClassZeroSms; - private static MediaPlayer [] sMediaPlayer; - private static final Object sLock = new Object(); - - public static final int DEBUG_SOUND_SERVER_REQUEST = 0; - public static final int DEBUG_SOUND_DB_OP = 1; - - public static void maybePlayDebugNoise(final Context context, final int sound) { - if (sDebugNoise) { - synchronized (sLock) { - try { - if (sMediaPlayer == null) { - sMediaPlayer = new MediaPlayer[2]; - sMediaPlayer[DEBUG_SOUND_SERVER_REQUEST] = - MediaPlayer.create(context, R.raw.server_request_debug); - sMediaPlayer[DEBUG_SOUND_DB_OP] = - MediaPlayer.create(context, R.raw.db_op_debug); - sMediaPlayer[DEBUG_SOUND_DB_OP].setVolume(1.0F, 1.0F); - sMediaPlayer[DEBUG_SOUND_SERVER_REQUEST].setVolume(0.3F, 0.3F); - } - if (sMediaPlayer[sound] != null) { - sMediaPlayer[sound].start(); - } - } catch (final IllegalArgumentException e) { - LogUtil.e(TAG, "MediaPlayer exception", e); - } catch (final SecurityException e) { - LogUtil.e(TAG, "MediaPlayer exception", e); - } catch (final IllegalStateException e) { - LogUtil.e(TAG, "MediaPlayer exception", e); - } - } - } - } - - public static boolean isDebugEnabled() { - return BugleGservices.get().getBoolean(BugleGservicesKeys.ENABLE_DEBUGGING_FEATURES, - BugleGservicesKeys.ENABLE_DEBUGGING_FEATURES_DEFAULT); - } - - public abstract static class DebugAction { - String mTitle; - public DebugAction(final String title) { - mTitle = title; - } - - @Override - public String toString() { - return mTitle; - } - - public abstract void run(); - } - - public static void showDebugOptions(final Activity host) { - final AlertDialog.Builder builder = new AlertDialog.Builder(host); - - final ArrayAdapter<DebugAction> arrayAdapter = new ArrayAdapter<DebugAction>( - host, android.R.layout.simple_list_item_1); - - arrayAdapter.add(new DebugAction("Dump Database") { - @Override - public void run() { - DumpDatabaseAction.dumpDatabase(); - } - }); - - arrayAdapter.add(new DebugAction("Log Telephony Data") { - @Override - public void run() { - LogTelephonyDatabaseAction.dumpDatabase(); - } - }); - - arrayAdapter.add(new DebugAction("Toggle Noise") { - @Override - public void run() { - sDebugNoise = !sDebugNoise; - } - }); - - arrayAdapter.add(new DebugAction("Force sync SMS") { - @Override - public void run() { - final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); - prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, -1); - SyncManager.forceSync(); - } - }); - - arrayAdapter.add(new DebugAction("Sync SMS") { - @Override - public void run() { - SyncManager.sync(); - } - }); - - arrayAdapter.add(new DebugAction("Load SMS/MMS from dump file") { - @Override - public void run() { - new DebugSmsMmsDumpTask(host, - DebugSmsMmsFromDumpFileDialogFragment.ACTION_LOAD).executeOnThreadPool(); - } - }); - - arrayAdapter.add(new DebugAction("Email SMS/MMS dump file") { - @Override - public void run() { - new DebugSmsMmsDumpTask(host, - DebugSmsMmsFromDumpFileDialogFragment.ACTION_EMAIL).executeOnThreadPool(); - } - }); - - arrayAdapter.add(new DebugAction("MMS Config...") { - @Override - public void run() { - UIIntents.get().launchDebugMmsConfigActivity(host); - } - }); - - arrayAdapter.add(new DebugAction(sDebugClassZeroSms ? "Turn off Class 0 sms test" : - "Turn on Class Zero test") { - @Override - public void run() { - sDebugClassZeroSms = !sDebugClassZeroSms; - } - }); - - builder.setAdapter(arrayAdapter, - new android.content.DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface arg0, final int pos) { - arrayAdapter.getItem(pos).run(); - } - }); - - builder.create().show(); - } - - /** - * Task to list all the dump files and perform an action on it - */ - private static class DebugSmsMmsDumpTask extends SafeAsyncTask<Void, Void, String[]> { - private final String mAction; - private final Activity mHost; - - public DebugSmsMmsDumpTask(final Activity host, final String action) { - mHost = host; - mAction = action; - } - - @Override - protected void onPostExecute(final String[] result) { - if (result == null || result.length < 1) { - return; - } - final FragmentManager fragmentManager = mHost.getFragmentManager(); - final FragmentTransaction ft = fragmentManager.beginTransaction(); - final DebugSmsMmsFromDumpFileDialogFragment dialog = - DebugSmsMmsFromDumpFileDialogFragment.newInstance(result, mAction); - dialog.show(fragmentManager, ""/*tag*/); - } - - @Override - protected String[] doInBackgroundTimed(final Void... params) { - final File dir = DebugUtils.getDebugFilesDir(); - return dir.list(new FilenameFilter() { - @Override - public boolean accept(final File dir, final String filename) { - return filename != null - && ((mAction == DebugSmsMmsFromDumpFileDialogFragment.ACTION_EMAIL - && filename.equals(DumpDatabaseAction.DUMP_NAME)) - || filename.startsWith(MmsUtils.MMS_DUMP_PREFIX) - || filename.startsWith(MmsUtils.SMS_DUMP_PREFIX)); - } - }); - } - } - - /** - * Dump the received raw SMS data into a file on external storage - * - * @param id The ID to use as part of the dump file name - * @param messages The raw SMS data - */ - public static void dumpSms(final long id, final android.telephony.SmsMessage[] messages, - final String format) { - try { - final String dumpFileName = MmsUtils.SMS_DUMP_PREFIX + Long.toString(id); - final File dumpFile = DebugUtils.getDebugFile(dumpFileName, true); - if (dumpFile != null) { - final FileOutputStream fos = new FileOutputStream(dumpFile); - final DataOutputStream dos = new DataOutputStream(fos); - try { - final int chars = (TextUtils.isEmpty(format) ? 0 : format.length()); - dos.writeInt(chars); - if (chars > 0) { - dos.writeUTF(format); - } - dos.writeInt(messages.length); - for (final android.telephony.SmsMessage message : messages) { - final byte[] pdu = message.getPdu(); - dos.writeInt(pdu.length); - dos.write(pdu, 0, pdu.length); - } - dos.flush(); - } finally { - dos.close(); - ensureReadable(dumpFile); - } - } - } catch (final IOException e) { - LogUtil.e(LogUtil.BUGLE_TAG, "dumpSms: " + e, e); - } - } - - /** - * Load MMS/SMS from the dump file - */ - public static SmsMessage[] retreiveSmsFromDumpFile(final String dumpFileName) { - SmsMessage[] messages = null; - final File inputFile = DebugUtils.getDebugFile(dumpFileName, false); - if (inputFile != null) { - FileInputStream fis = null; - DataInputStream dis = null; - try { - fis = new FileInputStream(inputFile); - dis = new DataInputStream(fis); - - // SMS dump - final int chars = dis.readInt(); - if (chars > 0) { - final String format = dis.readUTF(); - } - final int count = dis.readInt(); - final SmsMessage[] messagesTemp = new SmsMessage[count]; - for (int i = 0; i < count; i++) { - final int length = dis.readInt(); - final byte[] pdu = new byte[length]; - dis.read(pdu, 0, length); - messagesTemp[i] = SmsMessage.createFromPdu(pdu); - } - messages = messagesTemp; - } catch (final FileNotFoundException e) { - // Nothing to do - } catch (final StreamCorruptedException e) { - // Nothing to do - } catch (final IOException e) { - // Nothing to do - } finally { - if (dis != null) { - try { - dis.close(); - } catch (final IOException e) { - // Nothing to do - } - } - } - } - return messages; - } - - public static File getDebugFile(final String fileName, final boolean create) { - final File dir = getDebugFilesDir(); - final File file = new File(dir, fileName); - if (create && file.exists()) { - file.delete(); - } - return file; - } - - public static File getDebugFilesDir() { - final File dir = Environment.getExternalStorageDirectory(); - return dir; - } - - /** - * Load MMS/SMS from the dump file - */ - public static byte[] receiveFromDumpFile(final String dumpFileName) { - byte[] data = null; - try { - final File inputFile = getDebugFile(dumpFileName, false); - if (inputFile != null) { - final FileInputStream fis = new FileInputStream(inputFile); - final BufferedInputStream bis = new BufferedInputStream(fis); - try { - // dump file - data = ByteStreams.toByteArray(bis); - if (data == null || data.length < 1) { - LogUtil.e(LogUtil.BUGLE_TAG, "receiveFromDumpFile: empty data"); - } - } finally { - bis.close(); - } - } - } catch (final IOException e) { - LogUtil.e(LogUtil.BUGLE_TAG, "receiveFromDumpFile: " + e, e); - } - return data; - } - - public static void ensureReadable(final File file) { - if (file.exists()){ - file.setReadable(true, false); - } - } - - /** - * Logs the name of the method that is currently executing, e.g. "MyActivity.onCreate". This is - * useful for surgically adding logs for tracing execution while debugging. - * <p> - * NOTE: This method retrieves the current thread's stack trace, which adds runtime overhead. - * However, this method is only executed on eng builds if DEBUG logs are loggable. - */ - public static void logCurrentMethod(String tag) { - if (!LogUtil.isLoggable(tag, LogUtil.DEBUG)) { - return; - } - StackTraceElement caller = getCaller(1); - if (caller == null) { - return; - } - String className = caller.getClassName(); - // Strip off the package name - int lastDot = className.lastIndexOf('.'); - if (lastDot > -1) { - className = className.substring(lastDot + 1); - } - LogUtil.d(tag, className + "." + caller.getMethodName()); - } - - /** - * Returns info about the calling method. The {@code depth} parameter controls how far back to - * go. For example, if foo() calls bar(), and bar() calls getCaller(0), it returns info about - * bar(). If bar() instead called getCaller(1), it would return info about foo(). And so on. - * <p> - * NOTE: This method retrieves the current thread's stack trace, which adds runtime overhead. - * It should only be used in production where necessary to gather context about an error or - * unexpected event (e.g. the {@link Assert} class uses it). - * - * @return stack frame information for the caller (if found); otherwise {@code null}. - */ - public static StackTraceElement getCaller(int depth) { - // If the signature of this method is changed, proguard.flags must be updated! - if (depth < 0) { - throw new IllegalArgumentException("depth cannot be negative"); - } - StackTraceElement[] trace = Thread.currentThread().getStackTrace(); - if (trace == null || trace.length < (depth + 2)) { - return null; - } - // The stack trace includes some methods we don't care about (e.g. this method). - // Walk down until we find this method, and then back up to the caller we're looking for. - for (int i = 0; i < trace.length - 1; i++) { - String methodName = trace[i].getMethodName(); - if ("getCaller".equals(methodName)) { - return trace[i + depth + 1]; - } - } - // Never found ourself in the stack?! - return null; - } - - /** - * Returns a boolean indicating whether ClassZero debugging is enabled. If enabled, any received - * sms is treated as if it were a class zero message and displayed by the ClassZeroActivity. - */ - public static boolean debugClassZeroSmsEnabled() { - return sDebugClassZeroSms; - } -} diff --git a/src/com/android/messaging/util/EmailAddress.java b/src/com/android/messaging/util/EmailAddress.java deleted file mode 100644 index 0c0dab9..0000000 --- a/src/com/android/messaging/util/EmailAddress.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * 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.android.messaging.util; - -import com.google.common.base.CharMatcher; - -/** - * Parsing the email address - */ -public final class EmailAddress { - private static final CharMatcher ANY_WHITESPACE = CharMatcher.anyOf( - " \t\n\r\f\u000B\u0085\u2028\u2029\u200D\uFFEF\uFFFD\uFFFE\uFFFF"); - private static final CharMatcher EMAIL_ALLOWED_CHARS = CharMatcher.inRange((char) 0, (char) 31) - .or(CharMatcher.is((char) 127)) - .or(CharMatcher.anyOf(" @,:<>")) - .negate(); - - /** - * Helper method that checks whether the input text is valid email address. - * TODO: This creates a new EmailAddress object each time - * Need to make it more lightweight by pulling out the validation code into a static method. - */ - public static boolean isValidEmail(final String emailText) { - return new EmailAddress(emailText).isValid(); - } - - /** - * Parses the specified email address. Internationalized addresses are treated as invalid. - * - * @param emailString A string representing just an email address. It should - * not contain any other tokens. <code>"Name<foo@example.org>"</code> won't be valid. - */ - public EmailAddress(final String emailString) { - this(emailString, false); - } - - /** - * Parses the specified email address. - * - * @param emailString A string representing just an email address. It should - * not contain any other tokens. <code>"Name<foo@example.org>"</code> won't be valid. - * @param i18n Accept an internationalized address if it is true. - */ - public EmailAddress(final String emailString, final boolean i18n) { - allowI18n = i18n; - valid = parseEmail(emailString); - } - - /** - * Parses the specified email address. Internationalized addresses are treated as invalid. - * - * @param user A string representing the username in the email prior to the '@' symbol - * @param host A string representing the host following the '@' symbol - */ - public EmailAddress(final String user, final String host) { - this(user, host, false); - } - - /** - * Parses the specified email address. - * - * @param user A string representing the username in the email prior to the '@' symbol - * @param host A string representing the host following the '@' symbol - * @param i18n Accept an internationalized address if it is true. - */ - public EmailAddress(final String user, final String host, final boolean i18n) { - allowI18n = i18n; - this.user = user; - setHost(host); - } - - protected boolean parseEmail(final String emailString) { - // check for null - if (emailString == null) { - return false; - } - - // Check for an '@' character. Get the last one, in case the local part is - // quoted. See http://b/1944742. - final int atIndex = emailString.lastIndexOf('@'); - if ((atIndex <= 0) || // no '@' character in the email address - // or @ on the first position - (atIndex == (emailString.length() - 1))) { // last character, no host - return false; - } - - user = emailString.substring(0, atIndex); - host = emailString.substring(atIndex + 1); - - return isValidInternal(); - } - - @Override - public String toString() { - return user + "@" + host; - } - - /** - * Ensure the email address is valid, conforming to current RFC2821 and - * RFC2822 guidelines (although some iffy characters, like ! and ;, are - * allowed because they are not technically prohibited in the RFC) - */ - private boolean isValidInternal() { - if ((user == null) || (host == null)) { - return false; - } - - if ((user.length() == 0) || (host.length() == 0)) { - return false; - } - - // check for white space in the host - if (ANY_WHITESPACE.indexIn(host) >= 0) { - return false; - } - - // ensure the host is above the minimum length - if (host.length() < 4) { - return false; - } - - final int firstDot = host.indexOf('.'); - - // ensure host contains at least one dot - if (firstDot == -1) { - return false; - } - - // check if the host contains two continuous dots. - if (host.indexOf("..") >= 0) { - return false; - } - - // check if the first host char is a dot. - if (host.charAt(0) == '.') { - return false; - } - - final int secondDot = host.indexOf(".", firstDot + 1); - - // if there's a dot at the end, there needs to be a second dot - if (host.charAt(host.length() - 1) == '.' && secondDot == -1) { - return false; - } - - // Host must not have any disallowed characters; allowI18n dictates whether - // host must be ASCII. - if (!EMAIL_ALLOWED_CHARS.matchesAllOf(host) - || (!allowI18n && !CharMatcher.ASCII.matchesAllOf(host))) { - return false; - } - - if (user.startsWith("\"")) { - if (!isQuotedUserValid()) { - return false; - } - } else { - // check for white space in the user - if (ANY_WHITESPACE.indexIn(user) >= 0) { - return false; - } - - // the user cannot contain two continuous dots - if (user.indexOf("..") >= 0) { - return false; - } - - // User must not have any disallowed characters; allow I18n dictates whether - // user must be ASCII. - if (!EMAIL_ALLOWED_CHARS.matchesAllOf(user) - || (!allowI18n && !CharMatcher.ASCII.matchesAllOf(user))) { - return false; - } - } - return true; - } - - private boolean isQuotedUserValid() { - final int limit = user.length() - 1; - if (limit < 1 || !user.endsWith("\"")) { - return false; - } - - // Unusual loop bounds (looking only at characters between the outer quotes, - // not at either quote character). Plus, i is manipulated within the loop. - for (int i = 1; i < limit; ++i) { - final char ch = user.charAt(i); - if (ch == '"' || ch == 127 - // No non-whitespace control chars: - || (ch < 32 && !ANY_WHITESPACE.matches(ch)) - // No non-ASCII chars, unless i18n is in effect: - || (ch >= 128 && !allowI18n)) { - return false; - } else if (ch == '\\') { - if (i + 1 < limit) { - ++i; // Skip the quoted character - } else { - // We have a trailing backslash -- so it can't be quoting anything. - return false; - } - } - } - - return true; - } - - @Override - public boolean equals(final Object otherObject) { - // Do an instance check first as an optimization. - if (this == otherObject) { - return true; - } - if (otherObject instanceof EmailAddress) { - final EmailAddress otherAddress = (EmailAddress) otherObject; - return toString().equals(otherAddress.toString()); - } - return false; - } - - @Override - public int hashCode() { - // Arbitrary hash code as a function of both host and user. - return toString().hashCode(); - } - - // accessors - public boolean isValid() { - return valid; - } - - public String getUser() { - return user; - } - - public String getHost() { - return host; - } - - // used to change the host on an email address and rechecks validity - - /** - * Changes the host name of the email address and rechecks the address' - * validity. Exercise caution when storing EmailAddress instances in - * hash-keyed collections. Calling setHost() with a different host name will - * change the return value of hashCode. - * - * @param hostName The new host name of the email address. - */ - public void setHost(final String hostName) { - host = hostName; - valid = isValidInternal(); - } - - protected boolean valid = false; - protected String user = null; - protected String host = null; - protected boolean allowI18n = false; -} diff --git a/src/com/android/messaging/util/FallbackStrategies.java b/src/com/android/messaging/util/FallbackStrategies.java deleted file mode 100644 index 57c208f..0000000 --- a/src/com/android/messaging/util/FallbackStrategies.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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.android.messaging.util; - -import java.util.ArrayList; -import java.util.List; - -/** - * Provides a generic and loose-coupled framework to execute one primary and multiple fallback - * strategies for solving a given task.<p> - * Basically, what would have been a nasty try-catch that's hard to separate and maintain: - * <pre><code> - * try { - * // doSomething() that may fail. - * } catch (Exception ex) { - * try { - * // fallback1() that may fail. - * } catch (Exception ex2) { - * try { - * // fallback2() that may fail. - * } catch (Exception ex3) { - * // ... - * } - * } - * } - * </code></pre> - * Now becomes:<br> - * <pre><code> - * FallbackStrategies - * .startWith(something) - * .thenTry(fallback1) - * .thenTry(fallback2) - * .execute(); - * </code></pre> - */ -public class FallbackStrategies<Input, Output> { - public interface Strategy<Input, Output> { - Output execute(Input params) throws Exception; - } - - private final List<Strategy<Input, Output>> mChainedStrategies; - - private FallbackStrategies(final Strategy<Input, Output> primaryStrategy) { - mChainedStrategies = new ArrayList<Strategy<Input, Output>>(); - mChainedStrategies.add(primaryStrategy); - } - - public static <Input, Output> FallbackStrategies<Input, Output> startWith( - final Strategy<Input, Output> primaryStrategy) { - return new FallbackStrategies<Input, Output>(primaryStrategy); - } - - public FallbackStrategies<Input, Output> thenTry(final Strategy<Input, Output> strategy) { - Assert.isFalse(mChainedStrategies.isEmpty()); - mChainedStrategies.add(strategy); - return this; - } - - public Output execute(final Input params) { - final int count = mChainedStrategies.size(); - for (int i = 0; i < count; i++) { - final Strategy<Input, Output> strategy = mChainedStrategies.get(i); - try { - // If succeeds, this will directly return. - return strategy.execute(params); - } catch (Exception ex) { - LogUtil.e(LogUtil.BUGLE_TAG, "Exceptions occured when executing strategy " + - strategy + (i < count - 1 ? - ", attempting fallback " + mChainedStrategies.get(i + 1) : - ", and running out of fallbacks."), ex); - // This will fall through and continue with the next strategy (if any). - } - } - // Running out of strategies, return null. - // TODO: Should this accept user-defined fallback value other than null? - return null; - } -} diff --git a/src/com/android/messaging/util/FileUtil.java b/src/com/android/messaging/util/FileUtil.java deleted file mode 100644 index 7c47ae9..0000000 --- a/src/com/android/messaging/util/FileUtil.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; -import android.webkit.MimeTypeMap; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.google.common.io.Files; - -import java.io.File; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -public class FileUtil { - /** Returns a new file name, ensuring that such a file does not already exist. */ - private static synchronized File getNewFile(File directory, String extension, - String fileNameFormat) throws IOException { - final Date date = new Date(System.currentTimeMillis()); - final SimpleDateFormat dateFormat = new SimpleDateFormat(fileNameFormat); - final String numberedFileNameFormat = dateFormat.format(date) + "_%02d" + "." + extension; - for (int i = 1; i <= 99; i++) { // Only save 99 of the same file name. - final String newName = String.format(Locale.US, numberedFileNameFormat, i); - File testFile = new File(directory, newName); - if (!testFile.exists()) { - testFile.createNewFile(); - return testFile; - } - } - LogUtil.e(LogUtil.BUGLE_TAG, "Too many duplicate file names: " + numberedFileNameFormat); - return null; - } - - /** - * Creates an unused name to use for creating a new file. The format happens to be similar - * to that used by the Android camera application. - * - * @param directory directory that the file should be saved to - * @param contentType of the media being saved - * @return file name to be used for creating the new file. The caller is responsible for - * actually creating the file. - */ - public static File getNewFile(File directory, String contentType) throws IOException { - MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); - String fileExtension = mimeTypeMap.getExtensionFromMimeType(contentType); - - final Context context = Factory.get().getApplicationContext(); - String fileNameFormat = context.getString(ContentType.isImageType(contentType) - ? R.string.new_image_file_name_format : R.string.new_file_name_format); - return getNewFile(directory, fileExtension, fileNameFormat); - } - - /** Delete everything below and including root */ - public static void removeFileOrDirectory(File root) { - removeFileOrDirectoryExcept(root, null); - } - - /** Delete everything below and including root except for the given file */ - public static void removeFileOrDirectoryExcept(File root, File exclude) { - if (root.exists()) { - if (root.isDirectory()) { - for (File file : root.listFiles()) { - if (exclude == null || !file.equals(exclude)) { - removeFileOrDirectoryExcept(file, exclude); - } - } - root.delete(); - } else if (root.isFile()) { - root.delete(); - } - } - } - - /** - * Move all files and folders under a directory into the target. - */ - public static void moveAllContentUnderDirectory(File sourceDir, File targetDir) { - if (sourceDir.isDirectory() && targetDir.isDirectory()) { - if (isSameOrSubDirectory(sourceDir, targetDir)) { - LogUtil.e(LogUtil.BUGLE_TAG, "Can't move directory content since the source " + - "directory is a parent of the target"); - return; - } - for (File file : sourceDir.listFiles()) { - if (file.isDirectory()) { - final File dirTarget = new File(targetDir, file.getName()); - dirTarget.mkdirs(); - moveAllContentUnderDirectory(file, dirTarget); - } else { - try { - final File fileTarget = new File(targetDir, file.getName()); - Files.move(file, fileTarget); - } catch (IOException e) { - LogUtil.e(LogUtil.BUGLE_TAG, "Failed to move files", e); - // Try proceed with the next file. - } - } - } - } - } - - /** - * Checks, whether the child directory is the same as, or a sub-directory of the base - * directory. - */ - private static boolean isSameOrSubDirectory(File base, File child) { - try { - base = base.getCanonicalFile(); - child = child.getCanonicalFile(); - File parentFile = child; - while (parentFile != null) { - if (base.equals(parentFile)) { - return true; - } - parentFile = parentFile.getParentFile(); - } - return false; - } catch (IOException ex) { - LogUtil.e(LogUtil.BUGLE_TAG, "Error while accessing file", ex); - return false; - } - } -} diff --git a/src/com/android/messaging/util/GifTranscoder.java b/src/com/android/messaging/util/GifTranscoder.java deleted file mode 100644 index 65413a0..0000000 --- a/src/com/android/messaging/util/GifTranscoder.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; -import android.text.format.Formatter; - -import com.google.common.base.Stopwatch; - -import java.io.File; -import java.util.concurrent.TimeUnit; - -/** - * Compresses a GIF so it can be sent via MMS. - * <p> - * The entry point lives in its own class, we can defer loading the native GIF transcoding library - * into memory until we actually need it. - */ -public class GifTranscoder { - private static final String TAG = LogUtil.BUGLE_TAG; - - private static int MIN_HEIGHT = 100; - private static int MIN_WIDTH = 100; - - static { - System.loadLibrary("giftranscode"); - } - - public static boolean transcode(Context context, String filePath, String outFilePath) { - if (!isEnabled()) { - return false; - } - final long inputSize = new File(filePath).length(); - Stopwatch stopwatch = Stopwatch.createStarted(); - final boolean success = transcodeInternal(filePath, outFilePath); - stopwatch.stop(); - final long elapsedMs = stopwatch.elapsed(TimeUnit.MILLISECONDS); - final long outputSize = new File(outFilePath).length(); - final float compression = (inputSize > 0) ? ((float) outputSize / inputSize) : 0; - - if (success) { - LogUtil.i(TAG, String.format("Resized GIF (%s) in %d ms, %s => %s (%.0f%%)", - LogUtil.sanitizePII(filePath), - elapsedMs, - Formatter.formatShortFileSize(context, inputSize), - Formatter.formatShortFileSize(context, outputSize), - compression * 100.0f)); - } - return success; - } - - private static native boolean transcodeInternal(String filePath, String outFilePath); - - /** - * Estimates the size of a GIF transcoded from a GIF with the specified size. - */ - public static long estimateFileSizeAfterTranscode(long fileSize) { - // I tested transcoding on ~70 GIFs and found that the transcoded files are in general - // about 25-35% the size of the original. This compression ratio is very consistent for the - // class of GIFs we care about most: those converted from video clips and 1-3 MB in size. - return (long) (fileSize * 0.35f); - } - - public static boolean canBeTranscoded(int width, int height) { - if (!isEnabled()) { - return false; - } - return width >= MIN_WIDTH && height >= MIN_HEIGHT; - } - - private static boolean isEnabled() { - final boolean enabled = BugleGservices.get().getBoolean( - BugleGservicesKeys.ENABLE_GIF_TRANSCODING, - BugleGservicesKeys.ENABLE_GIF_TRANSCODING_DEFAULT); - if (!enabled) { - LogUtil.w(TAG, "GIF transcoding is disabled"); - } - return enabled; - } -} diff --git a/src/com/android/messaging/util/ImageUtils.java b/src/com/android/messaging/util/ImageUtils.java deleted file mode 100644 index 05d3678..0000000 --- a/src/com/android/messaging/util/ImageUtils.java +++ /dev/null @@ -1,908 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.app.ActivityManager; -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.BitmapShader; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Shader.TileMode; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.provider.MediaStore; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.view.View; - -import com.android.messaging.Factory; -import com.android.messaging.datamodel.MediaScratchFileProvider; -import com.android.messaging.datamodel.MessagingContentProvider; -import com.android.messaging.datamodel.media.ImageRequest; -import com.android.messaging.util.Assert.DoesNotRunOnMainThread; -import com.android.messaging.util.exif.ExifInterface; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.io.Files; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.Arrays; - -public class ImageUtils { - private static final String TAG = LogUtil.BUGLE_TAG; - private static final int MAX_OOM_COUNT = 1; - private static final byte[] GIF87_HEADER = "GIF87a".getBytes(Charset.forName("US-ASCII")); - private static final byte[] GIF89_HEADER = "GIF89a".getBytes(Charset.forName("US-ASCII")); - - // Used for drawBitmapWithCircleOnCanvas. - // Default color is transparent for both circle background and stroke. - public static final int DEFAULT_CIRCLE_BACKGROUND_COLOR = 0; - public static final int DEFAULT_CIRCLE_STROKE_COLOR = 0; - - private static volatile ImageUtils sInstance; - - public static ImageUtils get() { - if (sInstance == null) { - synchronized (ImageUtils.class) { - if (sInstance == null) { - sInstance = new ImageUtils(); - } - } - } - return sInstance; - } - - @VisibleForTesting - public static void set(final ImageUtils imageUtils) { - sInstance = imageUtils; - } - - /** - * Transforms a bitmap into a byte array. - * - * @param quality Value between 0 and 100 that the compressor uses to discern what quality the - * resulting bytes should be - * @param bitmap Bitmap to convert into bytes - * @return byte array of bitmap - */ - public static byte[] bitmapToBytes(final Bitmap bitmap, final int quality) - throws OutOfMemoryError { - boolean done = false; - int oomCount = 0; - byte[] imageBytes = null; - while (!done) { - try { - final ByteArrayOutputStream os = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.JPEG, quality, os); - imageBytes = os.toByteArray(); - done = true; - } catch (final OutOfMemoryError e) { - LogUtil.w(TAG, "OutOfMemory converting bitmap to bytes."); - oomCount++; - if (oomCount <= MAX_OOM_COUNT) { - Factory.get().reclaimMemory(); - } else { - done = true; - LogUtil.w(TAG, "Failed to convert bitmap to bytes. Out of Memory."); - } - throw e; - } - } - return imageBytes; - } - - /** - * Given the source bitmap and a canvas, draws the bitmap through a circular - * mask. Only draws a circle with diameter equal to the destination width. - * - * @param bitmap The source bitmap to draw. - * @param canvas The canvas to draw it on. - * @param source The source bound of the bitmap. - * @param dest The destination bound on the canvas. - * @param bitmapPaint Optional Paint object for the bitmap - * @param fillBackground when set, fill the circle with backgroundColor - * @param strokeColor draw a border outside the circle with strokeColor - */ - public static void drawBitmapWithCircleOnCanvas(final Bitmap bitmap, final Canvas canvas, - final RectF source, final RectF dest, @Nullable Paint bitmapPaint, - final boolean fillBackground, final int backgroundColor, int strokeColor) { - // Draw bitmap through shader first. - final BitmapShader shader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP); - final Matrix matrix = new Matrix(); - - // Fit bitmap to bounds. - matrix.setRectToRect(source, dest, Matrix.ScaleToFit.CENTER); - - shader.setLocalMatrix(matrix); - - if (bitmapPaint == null) { - bitmapPaint = new Paint(); - } - - bitmapPaint.setAntiAlias(true); - if (fillBackground) { - bitmapPaint.setColor(backgroundColor); - canvas.drawCircle(dest.centerX(), dest.centerX(), dest.width() / 2f, bitmapPaint); - } - - bitmapPaint.setShader(shader); - canvas.drawCircle(dest.centerX(), dest.centerX(), dest.width() / 2f, bitmapPaint); - bitmapPaint.setShader(null); - - if (strokeColor != 0) { - final Paint stroke = new Paint(); - stroke.setAntiAlias(true); - stroke.setColor(strokeColor); - stroke.setStyle(Paint.Style.STROKE); - final float strokeWidth = 6f; - stroke.setStrokeWidth(strokeWidth); - canvas.drawCircle(dest.centerX(), - dest.centerX(), - dest.width() / 2f - stroke.getStrokeWidth() / 2f, - stroke); - } - } - - /** - * Sets a drawable to the background of a view. setBackgroundDrawable() is deprecated since - * JB and replaced by setBackground(). - */ - @SuppressWarnings("deprecation") - public static void setBackgroundDrawableOnView(final View view, final Drawable drawable) { - if (OsUtil.isAtLeastJB()) { - view.setBackground(drawable); - } else { - view.setBackgroundDrawable(drawable); - } - } - - /** - * Based on the input bitmap bounds given by BitmapFactory.Options, compute the required - * sub-sampling size for loading a scaled down version of the bitmap to the required size - * @param options a BitmapFactory.Options instance containing the bounds info of the bitmap - * @param reqWidth the desired width of the bitmap. Can be ImageRequest.UNSPECIFIED_SIZE. - * @param reqHeight the desired height of the bitmap. Can be ImageRequest.UNSPECIFIED_SIZE. - * @return - */ - public int calculateInSampleSize( - final BitmapFactory.Options options, final int reqWidth, final int reqHeight) { - // Raw height and width of image - final int height = options.outHeight; - final int width = options.outWidth; - int inSampleSize = 1; - - final boolean checkHeight = reqHeight != ImageRequest.UNSPECIFIED_SIZE; - final boolean checkWidth = reqWidth != ImageRequest.UNSPECIFIED_SIZE; - if ((checkHeight && height > reqHeight) || - (checkWidth && width > reqWidth)) { - - final int halfHeight = height / 2; - final int halfWidth = width / 2; - - // Calculate the largest inSampleSize value that is a power of 2 and keeps both - // height and width larger than the requested height and width. - while ((!checkHeight || (halfHeight / inSampleSize) > reqHeight) - && (!checkWidth || (halfWidth / inSampleSize) > reqWidth)) { - inSampleSize *= 2; - } - } - - return inSampleSize; - } - - private static final String[] MEDIA_CONTENT_PROJECTION = new String[] { - MediaStore.MediaColumns.MIME_TYPE - }; - - private static final int INDEX_CONTENT_TYPE = 0; - - @DoesNotRunOnMainThread - public static String getContentType(final ContentResolver cr, final Uri uri) { - // Figure out the content type of media. - String contentType = null; - Cursor cursor = null; - if (UriUtil.isMediaStoreUri(uri)) { - try { - cursor = cr.query(uri, MEDIA_CONTENT_PROJECTION, null, null, null); - - if (cursor != null && cursor.moveToFirst()) { - contentType = cursor.getString(INDEX_CONTENT_TYPE); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - if (contentType == null) { - // Last ditch effort to get the content type. Look at the file extension. - contentType = ContentType.getContentTypeFromExtension(uri.toString(), - ContentType.IMAGE_UNSPECIFIED); - } - return contentType; - } - - /** - * @param context Android context - * @param uri Uri to the image data - * @return The exif orientation value for the image in the specified uri - */ - public static int getOrientation(final Context context, final Uri uri) { - try { - return getOrientation(context.getContentResolver().openInputStream(uri)); - } catch (FileNotFoundException e) { - LogUtil.e(TAG, "getOrientation couldn't open: " + uri, e); - } - return android.media.ExifInterface.ORIENTATION_UNDEFINED; - } - - /** - * @param inputStream The stream to the image file. Closed on completion - * @return The exif orientation value for the image in the specified stream - */ - public static int getOrientation(final InputStream inputStream) { - int orientation = android.media.ExifInterface.ORIENTATION_UNDEFINED; - if (inputStream != null) { - try { - final ExifInterface exifInterface = new ExifInterface(); - exifInterface.readExif(inputStream); - final Integer orientationValue = - exifInterface.getTagIntValue(ExifInterface.TAG_ORIENTATION); - if (orientationValue != null) { - orientation = orientationValue.intValue(); - } - } catch (IOException e) { - // If the image if GIF, PNG, or missing exif header, just use the defaults - } finally { - try { - if (inputStream != null) { - inputStream.close(); - } - } catch (IOException e) { - LogUtil.e(TAG, "getOrientation error closing input stream", e); - } - } - } - return orientation; - } - - /** - * Returns whether the resource is a GIF image. - */ - public static boolean isGif(String contentType, Uri contentUri) { - if (TextUtils.equals(contentType, ContentType.IMAGE_GIF)) { - return true; - } - if (ContentType.isImageType(contentType)) { - try { - ContentResolver contentResolver = Factory.get().getApplicationContext() - .getContentResolver(); - InputStream inputStream = contentResolver.openInputStream(contentUri); - return ImageUtils.isGif(inputStream); - } catch (Exception e) { - LogUtil.w(TAG, "Could not open GIF input stream", e); - } - } - // Assume anything with a non-image content type is not a GIF - return false; - } - - /** - * @param inputStream The stream to the image file. Closed on completion - * @return Whether the image stream represents a GIF - */ - public static boolean isGif(InputStream inputStream) { - if (inputStream != null) { - try { - byte[] gifHeaderBytes = new byte[6]; - int value = inputStream.read(gifHeaderBytes, 0, 6); - if (value == 6) { - return Arrays.equals(gifHeaderBytes, GIF87_HEADER) - || Arrays.equals(gifHeaderBytes, GIF89_HEADER); - } - } catch (IOException e) { - return false; - } finally { - try { - inputStream.close(); - } catch (IOException e) { - // Ignore - } - } - } - return false; - } - - /** - * Read an image and compress it to particular max dimensions and size. - * Used to ensure images can fit in an MMS. - * TODO: This uses memory very inefficiently as it processes the whole image as a unit - * (rather than slice by slice) but system JPEG functions do not support slicing and dicing. - */ - public static class ImageResizer { - - /** - * The quality parameter which is used to compress JPEG images. - */ - private static final int IMAGE_COMPRESSION_QUALITY = 95; - /** - * The minimum quality parameter which is used to compress JPEG images. - */ - private static final int MINIMUM_IMAGE_COMPRESSION_QUALITY = 50; - - /** - * Minimum factor to reduce quality value - */ - private static final double QUALITY_SCALE_DOWN_RATIO = 0.85f; - - /** - * Maximum passes through the resize loop before failing permanently - */ - private static final int NUMBER_OF_RESIZE_ATTEMPTS = 6; - - /** - * Amount to scale down the picture when it doesn't fit - */ - private static final float MIN_SCALE_DOWN_RATIO = 0.75f; - - /** - * When computing sampleSize target scaling of no more than this ratio - */ - private static final float MAX_TARGET_SCALE_FACTOR = 1.5f; - - - // Current sample size for subsampling image during initial decode - private int mSampleSize; - // Current bitmap holding initial decoded source image - private Bitmap mDecoded; - // If scaling is needed this holds the scaled bitmap (else should equal mDecoded) - private Bitmap mScaled; - // Current JPEG compression quality to use when compressing image - private int mQuality; - // Current factor to scale down decoded image before compressing - private float mScaleFactor; - // Flag keeping track of whether cache memory has been reclaimed - private boolean mHasReclaimedMemory; - - // Initial size of the image (typically provided but can be UNSPECIFIED_SIZE) - private int mWidth; - private int mHeight; - // Orientation params of image as read from EXIF data - private final ExifInterface.OrientationParams mOrientationParams; - // Matrix to undo orientation and scale at the same time - private final Matrix mMatrix; - // Size limit as provided by MMS library - private final int mWidthLimit; - private final int mHeightLimit; - private final int mByteLimit; - // Uri from which to read source image - private final Uri mUri; - // Application context - private final Context mContext; - // Cached value of bitmap factory options - private final BitmapFactory.Options mOptions; - private final String mContentType; - - private final int mMemoryClass; - - /** - * Return resized (compressed) image (else null) - * - * @param width The width of the image (if known) - * @param height The height of the image (if known) - * @param orientation The orientation of the image as an ExifInterface constant - * @param widthLimit The width limit, in pixels - * @param heightLimit The height limit, in pixels - * @param byteLimit The binary size limit, in bytes - * @param uri Uri to the image data - * @param context Needed to open the image - * @param contentType of image - * @return encoded image meeting size requirements else null - */ - public static byte[] getResizedImageData(final int width, final int height, - final int orientation, final int widthLimit, final int heightLimit, - final int byteLimit, final Uri uri, final Context context, - final String contentType) { - final ImageResizer resizer = new ImageResizer(width, height, orientation, - widthLimit, heightLimit, byteLimit, uri, context, contentType); - return resizer.resize(); - } - - /** - * Create and initialize an image resizer - */ - private ImageResizer(final int width, final int height, final int orientation, - final int widthLimit, final int heightLimit, final int byteLimit, final Uri uri, - final Context context, final String contentType) { - mWidth = width; - mHeight = height; - mOrientationParams = ExifInterface.getOrientationParams(orientation); - mMatrix = new Matrix(); - mWidthLimit = widthLimit; - mHeightLimit = heightLimit; - mByteLimit = byteLimit; - mUri = uri; - mWidth = width; - mContext = context; - mQuality = IMAGE_COMPRESSION_QUALITY; - mScaleFactor = 1.0f; - mHasReclaimedMemory = false; - mOptions = new BitmapFactory.Options(); - mOptions.inScaled = false; - mOptions.inDensity = 0; - mOptions.inTargetDensity = 0; - mOptions.inSampleSize = 1; - mOptions.inJustDecodeBounds = false; - mOptions.inMutable = false; - final ActivityManager am = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - mMemoryClass = Math.max(16, am.getMemoryClass()); - mContentType = contentType; - } - - /** - * Try to compress the image - * - * @return encoded image meeting size requirements else null - */ - private byte[] resize() { - return ImageUtils.isGif(mContentType, mUri) ? resizeGifImage() : resizeStaticImage(); - } - - private byte[] resizeGifImage() { - byte[] bytesToReturn = null; - final String inputFilePath; - if (MediaScratchFileProvider.isMediaScratchSpaceUri(mUri)) { - inputFilePath = MediaScratchFileProvider.getFileFromUri(mUri).getAbsolutePath(); - } else { - if (!TextUtils.equals(mUri.getScheme(), ContentResolver.SCHEME_FILE)) { - Assert.fail("Expected a GIF file uri, but actual uri = " + mUri.toString()); - } - inputFilePath = mUri.getPath(); - } - - if (GifTranscoder.canBeTranscoded(mWidth, mHeight)) { - // Needed to perform the transcoding so that the gif can continue to play in the - // conversation while the sending is taking place - final Uri tmpUri = MediaScratchFileProvider.buildMediaScratchSpaceUri("gif"); - final File outputFile = MediaScratchFileProvider.getFileFromUri(tmpUri); - final String outputFilePath = outputFile.getAbsolutePath(); - - final boolean success = - GifTranscoder.transcode(mContext, inputFilePath, outputFilePath); - if (success) { - try { - bytesToReturn = Files.toByteArray(outputFile); - } catch (IOException e) { - LogUtil.e(TAG, "Could not create FileInputStream with path of " - + outputFilePath, e); - } - } - - // Need to clean up the new file created to compress the gif - mContext.getContentResolver().delete(tmpUri, null, null); - } else { - // We don't want to transcode the gif because its image dimensions would be too - // small so just return the bytes of the original gif - try { - bytesToReturn = Files.toByteArray(new File(inputFilePath)); - } catch (IOException e) { - LogUtil.e(TAG, - "Could not create FileInputStream with path of " + inputFilePath, e); - } - } - - return bytesToReturn; - } - - private byte[] resizeStaticImage() { - if (!ensureImageSizeSet()) { - // Cannot read image size - return null; - } - // Find incoming image size - if (!canBeCompressed()) { - return null; - } - - // Decode image - if out of memory - reclaim memory and retry - try { - for (int attempts = 0; attempts < NUMBER_OF_RESIZE_ATTEMPTS; attempts++) { - final byte[] encoded = recodeImage(attempts); - - // Only return data within the limit - if (encoded != null && encoded.length <= mByteLimit) { - return encoded; - } else { - final int currentSize = (encoded == null ? 0 : encoded.length); - updateRecodeParameters(currentSize); - } - } - } catch (final FileNotFoundException e) { - LogUtil.e(TAG, "File disappeared during resizing"); - } finally { - // Release all bitmaps - if (mScaled != null && mScaled != mDecoded) { - mScaled.recycle(); - } - if (mDecoded != null) { - mDecoded.recycle(); - } - } - return null; - } - - /** - * Ensure that the width and height of the source image are known - * @return flag indicating whether size is known - */ - private boolean ensureImageSizeSet() { - if (mWidth == MessagingContentProvider.UNSPECIFIED_SIZE || - mHeight == MessagingContentProvider.UNSPECIFIED_SIZE) { - // First get the image data (compressed) - final ContentResolver cr = mContext.getContentResolver(); - InputStream inputStream = null; - // Find incoming image size - try { - mOptions.inJustDecodeBounds = true; - inputStream = cr.openInputStream(mUri); - BitmapFactory.decodeStream(inputStream, null, mOptions); - - mWidth = mOptions.outWidth; - mHeight = mOptions.outHeight; - mOptions.inJustDecodeBounds = false; - - return true; - } catch (final FileNotFoundException e) { - LogUtil.e(TAG, "Could not open file corresponding to uri " + mUri, e); - } catch (final NullPointerException e) { - LogUtil.e(TAG, "NPE trying to open the uri " + mUri, e); - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (final IOException e) { - // Nothing to do - } - } - } - - return false; - } - return true; - } - - /** - * Choose an initial subsamplesize that ensures the decoded image is no more than - * MAX_TARGET_SCALE_FACTOR bigger than largest supported image and that it is likely to - * compress to smaller than the target size (assuming compression down to 1 bit per pixel). - * @return whether the image can be down subsampled - */ - private boolean canBeCompressed() { - final boolean logv = LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE); - - int imageHeight = mHeight; - int imageWidth = mWidth; - - // Assume can use half working memory to decode the initial image (4 bytes per pixel) - final int workingMemoryPixelLimit = (mMemoryClass * 1024 * 1024 / 8); - // Target 1 bits per pixel in final compressed image - final int finalSizePixelLimit = mByteLimit * 8; - // When choosing to halve the resolution - only do so the image will still be too big - // after scaling by MAX_TARGET_SCALE_FACTOR - final int heightLimitWithSlop = (int) (mHeightLimit * MAX_TARGET_SCALE_FACTOR); - final int widthLimitWithSlop = (int) (mWidthLimit * MAX_TARGET_SCALE_FACTOR); - final int pixelLimitWithSlop = (int) (finalSizePixelLimit * - MAX_TARGET_SCALE_FACTOR * MAX_TARGET_SCALE_FACTOR); - final int pixelLimit = Math.min(pixelLimitWithSlop, workingMemoryPixelLimit); - - int sampleSize = 1; - boolean fits = (imageHeight < heightLimitWithSlop && - imageWidth < widthLimitWithSlop && - imageHeight * imageWidth < pixelLimit); - - // Compare sizes to compute sub-sampling needed - while (!fits) { - sampleSize = sampleSize * 2; - // Note that recodeImage may try using mSampleSize * 2. Hence we use the factor of 4 - if (sampleSize >= (Integer.MAX_VALUE / 4)) { - LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, String.format( - "Cannot resize image: widthLimit=%d heightLimit=%d byteLimit=%d " + - "imageWidth=%d imageHeight=%d", mWidthLimit, mHeightLimit, mByteLimit, - mWidth, mHeight)); - Assert.fail("Image cannot be resized"); // http://b/18926934 - return false; - } - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "computeInitialSampleSize: Increasing sampleSize to " + sampleSize - + " as h=" + imageHeight + " vs " + heightLimitWithSlop - + " w=" + imageWidth + " vs " + widthLimitWithSlop - + " p=" + imageHeight * imageWidth + " vs " + pixelLimit); - } - imageHeight = mHeight / sampleSize; - imageWidth = mWidth / sampleSize; - fits = (imageHeight < heightLimitWithSlop && - imageWidth < widthLimitWithSlop && - imageHeight * imageWidth < pixelLimit); - } - - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "computeInitialSampleSize: Initial sampleSize " + sampleSize - + " for h=" + imageHeight + " vs " + heightLimitWithSlop - + " w=" + imageWidth + " vs " + widthLimitWithSlop - + " p=" + imageHeight * imageWidth + " vs " + pixelLimit); - } - - mSampleSize = sampleSize; - return true; - } - - /** - * Recode the image from initial Uri to encoded JPEG - * @param attempt Attempt number - * @return encoded image - */ - private byte[] recodeImage(final int attempt) throws FileNotFoundException { - byte[] encoded = null; - try { - final ContentResolver cr = mContext.getContentResolver(); - final boolean logv = LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE); - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "getResizedImageData: attempt=" + attempt - + " limit (w=" + mWidthLimit + " h=" + mHeightLimit + ") quality=" - + mQuality + " scale=" + mScaleFactor + " sampleSize=" + mSampleSize); - } - if (mScaled == null) { - if (mDecoded == null) { - mOptions.inSampleSize = mSampleSize; - final InputStream inputStream = cr.openInputStream(mUri); - mDecoded = BitmapFactory.decodeStream(inputStream, null, mOptions); - if (mDecoded == null) { - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData: got empty decoded bitmap"); - } - return null; - } - } - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "getResizedImageData: decoded w,h=" - + mDecoded.getWidth() + "," + mDecoded.getHeight()); - } - // Make sure to scale the decoded image if dimension is not within limit - final int decodedWidth = mDecoded.getWidth(); - final int decodedHeight = mDecoded.getHeight(); - if (decodedWidth > mWidthLimit || decodedHeight > mHeightLimit) { - final float minScaleFactor = Math.max( - mWidthLimit == 0 ? 1.0f : - (float) decodedWidth / (float) mWidthLimit, - mHeightLimit == 0 ? 1.0f : - (float) decodedHeight / (float) mHeightLimit); - if (mScaleFactor < minScaleFactor) { - mScaleFactor = minScaleFactor; - } - } - if (mScaleFactor > 1.0 || mOrientationParams.rotation != 0) { - mMatrix.reset(); - mMatrix.postRotate(mOrientationParams.rotation); - mMatrix.postScale(mOrientationParams.scaleX / mScaleFactor, - mOrientationParams.scaleY / mScaleFactor); - mScaled = Bitmap.createBitmap(mDecoded, 0, 0, decodedWidth, decodedHeight, - mMatrix, false /* filter */); - if (mScaled == null) { - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData: got empty scaled bitmap"); - } - return null; - } - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "getResizedImageData: scaled w,h=" - + mScaled.getWidth() + "," + mScaled.getHeight()); - } - } else { - mScaled = mDecoded; - } - } - // Now encode it at current quality - encoded = ImageUtils.bitmapToBytes(mScaled, mQuality); - if (encoded != null && logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData: Encoded down to " + encoded.length + "@" - + mScaled.getWidth() + "/" + mScaled.getHeight() + "~" - + mQuality); - } - } catch (final OutOfMemoryError e) { - LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData - image too big (OutOfMemoryError), will try " - + " with smaller scale factor"); - // fall through and keep trying with more compression - } - return encoded; - } - - /** - * When image recode fails this method updates compression parameters for the next attempt - * @param currentSize encoded image size (will be 0 if OOM) - */ - private void updateRecodeParameters(final int currentSize) { - final boolean logv = LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE); - // Only return data within the limit - if (currentSize > 0 && - mQuality > MINIMUM_IMAGE_COMPRESSION_QUALITY) { - // First if everything succeeded but failed to hit target size - // Try quality proportioned to sqrt of size over size limit - mQuality = Math.max(MINIMUM_IMAGE_COMPRESSION_QUALITY, - Math.min((int) (mQuality * Math.sqrt((1.0 * mByteLimit) / currentSize)), - (int) (mQuality * QUALITY_SCALE_DOWN_RATIO))); - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData: Retrying at quality " + mQuality); - } - } else if (currentSize > 0 && - mScaleFactor < 2.0 * MIN_SCALE_DOWN_RATIO * MIN_SCALE_DOWN_RATIO) { - // JPEG compression failed to hit target size - need smaller image - // First try scaling by a little (< factor of 2) just so long resulting scale down - // ratio is still significantly bigger than next subsampling step - // i.e. mScaleFactor/MIN_SCALE_DOWN_RATIO (new scaling factor) < - // 2.0 / MIN_SCALE_DOWN_RATIO (arbitrary limit) - mQuality = IMAGE_COMPRESSION_QUALITY; - mScaleFactor = mScaleFactor / MIN_SCALE_DOWN_RATIO; - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData: Retrying at scale " + mScaleFactor); - } - // Release scaled bitmap to trigger rescaling - if (mScaled != null && mScaled != mDecoded) { - mScaled.recycle(); - } - mScaled = null; - } else if (currentSize <= 0 && !mHasReclaimedMemory) { - // Then before we subsample try cleaning up our cached memory - Factory.get().reclaimMemory(); - mHasReclaimedMemory = true; - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData: Retrying after reclaiming memory "); - } - } else { - // Last resort - subsample image by another factor of 2 and try again - mSampleSize = mSampleSize * 2; - mQuality = IMAGE_COMPRESSION_QUALITY; - mScaleFactor = 1.0f; - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData: Retrying at sampleSize " + mSampleSize); - } - // Release all bitmaps to trigger subsampling - if (mScaled != null && mScaled != mDecoded) { - mScaled.recycle(); - } - mScaled = null; - if (mDecoded != null) { - mDecoded.recycle(); - mDecoded = null; - } - } - } - } - - /** - * Scales and center-crops a bitmap to the size passed in and returns the new bitmap. - * - * @param source Bitmap to scale and center-crop - * @param newWidth destination width - * @param newHeight destination height - * @return Bitmap scaled and center-cropped bitmap - */ - public static Bitmap scaleCenterCrop(final Bitmap source, final int newWidth, - final int newHeight) { - final int sourceWidth = source.getWidth(); - final int sourceHeight = source.getHeight(); - - // Compute the scaling factors to fit the new height and width, respectively. - // To cover the final image, the final scaling will be the bigger - // of these two. - final float xScale = (float) newWidth / sourceWidth; - final float yScale = (float) newHeight / sourceHeight; - final float scale = Math.max(xScale, yScale); - - // Now get the size of the source bitmap when scaled - final float scaledWidth = scale * sourceWidth; - final float scaledHeight = scale * sourceHeight; - - // Let's find out the upper left coordinates if the scaled bitmap - // should be centered in the new size give by the parameters - final float left = (newWidth - scaledWidth) / 2; - final float top = (newHeight - scaledHeight) / 2; - - // The target rectangle for the new, scaled version of the source bitmap will now - // be - final RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight); - - // Finally, we create a new bitmap of the specified size and draw our new, - // scaled bitmap onto it. - final Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, source.getConfig()); - final Canvas canvas = new Canvas(dest); - canvas.drawBitmap(source, null, targetRect, null); - - return dest; - } - - /** - * The drawable can be a Nine-Patch. If we directly use the same drawable instance for each - * drawable of different sizes, then the drawable sizes would interfere with each other. The - * solution here is to create a new drawable instance for every time with the SAME - * ConstantState (i.e. sharing the same common state such as the bitmap, so that we don't have - * to recreate the bitmap resource), and apply the different properties on top (nine-patch - * size and color tint). - * - * TODO: we are creating new drawable instances here, but there are optimizations that - * can be made. For example, message bubbles shouldn't need the mutate() call and the - * play/pause buttons shouldn't need to create new drawable from the constant state. - */ - public static Drawable getTintedDrawable(final Context context, final Drawable drawable, - final int color) { - // For some reason occassionally drawables on JB has a null constant state - final Drawable.ConstantState constantStateDrawable = drawable.getConstantState(); - final Drawable retDrawable = (constantStateDrawable != null) - ? constantStateDrawable.newDrawable(context.getResources()).mutate() - : drawable; - retDrawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); - return retDrawable; - } - - /** - * Decodes image resource header and returns the image size. - */ - public static Rect decodeImageBounds(final Context context, final Uri imageUri) { - final ContentResolver cr = context.getContentResolver(); - try { - final InputStream inputStream = cr.openInputStream(imageUri); - if (inputStream != null) { - try { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(inputStream, null, options); - return new Rect(0, 0, options.outWidth, options.outHeight); - } finally { - try { - inputStream.close(); - } catch (IOException e) { - // Do nothing. - } - } - } - } catch (FileNotFoundException e) { - LogUtil.e(TAG, "Couldn't open input stream for uri = " + imageUri); - } - return new Rect(0, 0, ImageRequest.UNSPECIFIED_SIZE, ImageRequest.UNSPECIFIED_SIZE); - } -} diff --git a/src/com/android/messaging/util/ImeUtil.java b/src/com/android/messaging/util/ImeUtil.java deleted file mode 100644 index ab0df13..0000000 --- a/src/com/android/messaging/util/ImeUtil.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.view.View; -import android.view.inputmethod.InputMethodManager; - -import com.google.common.annotations.VisibleForTesting; - -public class ImeUtil { - public interface ImeStateObserver { - void onImeStateChanged(boolean imeOpen); - } - - public interface ImeStateHost { - void onDisplayHeightChanged(int heightMeasureSpec); - void registerImeStateObserver(ImeUtil.ImeStateObserver observer); - void unregisterImeStateObserver(ImeUtil.ImeStateObserver observer); - boolean isImeOpen(); - } - - private static volatile ImeUtil sInstance; - - // Used to clear the static cached instance of ImeUtil during testing. This is necessary - // because a previous test may have installed a mocked instance (or vice versa). - public static void clearInstance() { - sInstance = null; - } - public static ImeUtil get() { - if (sInstance == null) { - synchronized (ImeUtil.class) { - if (sInstance == null) { - sInstance = new ImeUtil(); - } - } - } - return sInstance; - } - - @VisibleForTesting - public static void set(final ImeUtil imeUtil) { - sInstance = imeUtil; - } - - public void hideImeKeyboard(@NonNull final Context context, @NonNull final View v) { - Assert.notNull(context); - Assert.notNull(v); - - final InputMethodManager inputMethodManager = - (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - if (inputMethodManager != null) { - inputMethodManager.hideSoftInputFromWindow(v.getWindowToken(), 0 /* flags */); - } - } - - public void showImeKeyboard(@NonNull final Context context, @NonNull final View v) { - Assert.notNull(context); - Assert.notNull(v); - - final InputMethodManager inputMethodManager = - (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - if (inputMethodManager != null) { - v.requestFocus(); - inputMethodManager.showSoftInput(v, 0 /* flags */); - } - } - - public static void hideSoftInput(@NonNull final Context context, @NonNull final View v) { - final InputMethodManager inputMethodManager = - (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow(v.getWindowToken(), 0); - } -} diff --git a/src/com/android/messaging/util/LogSaver.java b/src/com/android/messaging/util/LogSaver.java deleted file mode 100644 index 7d1f2fd..0000000 --- a/src/com/android/messaging/util/LogSaver.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.os.Process; -import android.util.Log; - -import com.android.messaging.Factory; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.io.PrintWriter; -import java.text.SimpleDateFormat; -import java.util.logging.FileHandler; -import java.util.logging.Formatter; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Save the app's own log to dump along with adb bugreport - */ -public abstract class LogSaver { - /** - * Writes the accumulated log entries, from oldest to newest, to the specified PrintWriter. - * Log lines are emitted in much the same form as logcat -v threadtime -- specifically, - * lines will include a timestamp, pid, tid, level, and tag. - * - * @param writer The PrintWriter to output - */ - public abstract void dump(PrintWriter writer); - - /** - * Log a line - * - * @param level The log level to use - * @param tag The log tag - * @param msg The message of the log line - */ - public abstract void log(int level, String tag, String msg); - - /** - * Check if the LogSaver still matches the current Gservices settings - * - * @return true if matches, false otherwise - */ - public abstract boolean isCurrent(); - - private LogSaver() { - } - - public static LogSaver newInstance() { - final boolean persistent = BugleGservices.get().getBoolean( - BugleGservicesKeys.PERSISTENT_LOGSAVER, - BugleGservicesKeys.PERSISTENT_LOGSAVER_DEFAULT); - if (persistent) { - final int setSize = BugleGservices.get().getInt( - BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE, - BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE_DEFAULT); - final int fileLimitBytes = BugleGservices.get().getInt( - BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES, - BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES_DEFAULT); - return new DiskLogSaver(setSize, fileLimitBytes); - } else { - final int size = BugleGservices.get().getInt( - BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT, - BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT_DEFAULT); - return new MemoryLogSaver(size); - } - } - - /** - * A circular in-memory log to be used to log potentially verbose logs. The logs will be - * persisted in memory in the application and can be dumped by various dump() methods. - * For example, adb shell dumpsys activity provider com.android.messaging. - * The dump will also show up in bugreports. - */ - private static final class MemoryLogSaver extends LogSaver { - /** - * Record to store a single log entry. Stores timestamp, tid, level, tag, and message. - * It can be reused when the circular log rolls over. This avoids creating new objects. - */ - private static class LogRecord { - int mTid; - String mLevelString; - long mTimeMillis; // from System.currentTimeMillis - String mTag; - String mMessage; - - LogRecord() { - } - - void set(int tid, int level, long time, String tag, String message) { - this.mTid = tid; - this.mTimeMillis = time; - this.mTag = tag; - this.mMessage = message; - this.mLevelString = getLevelString(level); - } - } - - private final int mSize; - private final CircularArray<LogRecord> mLogList; - private final Object mLock; - - private final SimpleDateFormat mSdf = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); - - public MemoryLogSaver(final int size) { - mSize = size; - mLogList = new CircularArray<LogRecord>(size); - mLock = new Object(); - } - - @Override - public void dump(PrintWriter writer) { - int pid = Process.myPid(); - synchronized (mLock) { - for (int i = 0; i < mLogList.count(); i++) { - LogRecord rec = mLogList.get(i); - writer.println(String.format("%s %5d %5d %s %s: %s", - mSdf.format(rec.mTimeMillis), - pid, rec.mTid, rec.mLevelString, rec.mTag, rec.mMessage)); - } - } - } - - @Override - public void log(int level, String tag, String msg) { - synchronized (mLock) { - LogRecord rec = mLogList.getFree(); - if (rec == null) { - rec = new LogRecord(); - } - rec.set(Process.myTid(), level, System.currentTimeMillis(), tag, msg); - mLogList.add(rec); - } - } - - @Override - public boolean isCurrent() { - final boolean persistent = BugleGservices.get().getBoolean( - BugleGservicesKeys.PERSISTENT_LOGSAVER, - BugleGservicesKeys.PERSISTENT_LOGSAVER_DEFAULT); - if (persistent) { - return false; - } - final int size = BugleGservices.get().getInt( - BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT, - BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT_DEFAULT); - return size == mSize; - } - } - - /** - * A persistent, on-disk log saver. It uses the standard Java util logger along with - * a rotation log file set to store the logs in app's local file directory "app_logs". - */ - private static final class DiskLogSaver extends LogSaver { - private static final String DISK_LOG_DIR_NAME = "logs"; - - private final int mSetSize; - private final int mFileLimitBytes; - private Logger mDiskLogger; - - public DiskLogSaver(final int setSize, final int fileLimitBytes) { - Assert.isTrue(setSize > 0); - Assert.isTrue(fileLimitBytes > 0); - mSetSize = setSize; - mFileLimitBytes = fileLimitBytes; - initDiskLog(); - } - - private static void clearDefaultHandlers(Logger logger) { - Assert.notNull(logger); - for (Handler handler : logger.getHandlers()) { - logger.removeHandler(handler); - } - } - - private void initDiskLog() { - mDiskLogger = Logger.getLogger(LogUtil.BUGLE_TAG); - // We don't want the default console handler - clearDefaultHandlers(mDiskLogger); - // Don't want duplicate print in system log - mDiskLogger.setUseParentHandlers(false); - // FileHandler manages the log files in a fixed rotation set - final File logDir = Factory.get().getApplicationContext().getDir( - DISK_LOG_DIR_NAME, 0/*mode*/); - FileHandler handler = null; - try { - handler = new FileHandler( - logDir + "/%g.log", mFileLimitBytes, mSetSize, true/*append*/); - } catch (Exception e) { - Log.e(LogUtil.BUGLE_TAG, "LogSaver: fail to init disk logger", e); - return; - } - final Formatter formatter = new Formatter() { - @Override - public String format(java.util.logging.LogRecord r) { - return r.getMessage(); - } - }; - handler.setFormatter(formatter); - handler.setLevel(Level.ALL); - mDiskLogger.addHandler(handler); - } - - @Override - public void dump(PrintWriter writer) { - for (int i = mSetSize - 1; i >= 0; i--) { - final File logDir = Factory.get().getApplicationContext().getDir( - DISK_LOG_DIR_NAME, 0/*mode*/); - final String logFilePath = logDir + "/" + i + ".log"; - try { - final File logFile = new File(logFilePath); - if (!logFile.exists()) { - continue; - } - final BufferedReader reader = new BufferedReader(new FileReader(logFile)); - for (String line; (line = reader.readLine()) != null;) { - line = line.trim(); - writer.println(line); - } - } catch (FileNotFoundException e) { - Log.w(LogUtil.BUGLE_TAG, "LogSaver: can not find log file " + logFilePath); - } catch (IOException e) { - Log.w(LogUtil.BUGLE_TAG, "LogSaver: can not read log file", e); - } - } - } - - @Override - public void log(int level, String tag, String msg) { - final SimpleDateFormat sdf = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); - mDiskLogger.info(String.format("%s %5d %5d %s %s: %s\n", - sdf.format(System.currentTimeMillis()), - Process.myPid(), Process.myTid(), getLevelString(level), tag, msg)); - } - - @Override - public boolean isCurrent() { - final boolean persistent = BugleGservices.get().getBoolean( - BugleGservicesKeys.PERSISTENT_LOGSAVER, - BugleGservicesKeys.PERSISTENT_LOGSAVER_DEFAULT); - if (!persistent) { - return false; - } - final int setSize = BugleGservices.get().getInt( - BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE, - BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE_DEFAULT); - final int fileLimitBytes = BugleGservices.get().getInt( - BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES, - BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES_DEFAULT); - return setSize == mSetSize && fileLimitBytes == mFileLimitBytes; - } - } - - private static String getLevelString(final int level) { - switch (level) { - case android.util.Log.DEBUG: - return "D"; - case android.util.Log.WARN: - return "W"; - case android.util.Log.INFO: - return "I"; - case android.util.Log.VERBOSE: - return "V"; - case android.util.Log.ERROR: - return "E"; - case android.util.Log.ASSERT: - return "A"; - default: - return "?"; - } - } -} diff --git a/src/com/android/messaging/util/LogUtil.java b/src/com/android/messaging/util/LogUtil.java deleted file mode 100644 index 021f39b..0000000 --- a/src/com/android/messaging/util/LogUtil.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * 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.android.messaging.util; - -/** - * Log utility class. - */ -public class LogUtil { - public static final String BUGLE_TAG = "MessagingApp"; - public static final String PROFILE_TAG = "MessagingAppProf"; - public static final String BUGLE_DATABASE_TAG = "MessagingAppDb"; - public static final String BUGLE_DATABASE_PERF_TAG = "MessagingAppDbPerf"; - public static final String BUGLE_DATAMODEL_TAG = "MessagingAppDataModel"; - public static final String BUGLE_IMAGE_TAG = "MessagingAppImage"; - public static final String BUGLE_NOTIFICATIONS_TAG = "MessagingAppNotif"; - public static final String BUGLE_WIDGET_TAG = "MessagingAppWidget"; - - public static final int DEBUG = android.util.Log.DEBUG; - public static final int WARN = android.util.Log.WARN; - public static final int VERBOSE = android.util.Log.VERBOSE; - public static final int INFO = android.util.Log.INFO; - public static final int ERROR = android.util.Log.ERROR; - - // If this is non-null, DEBUG and higher logs will be tracked in-memory. It will not include - // VERBOSE logs. - private static LogSaver sDebugLogSaver; - private static volatile boolean sCaptureDebugLogs; - - /** - * Read Gservices to see if logging should be enabled. - */ - public static void refreshGservices(final BugleGservices gservices) { - sCaptureDebugLogs = gservices.getBoolean( - BugleGservicesKeys.ENABLE_LOG_SAVER, - BugleGservicesKeys.ENABLE_LOG_SAVER_DEFAULT); - if (sCaptureDebugLogs && (sDebugLogSaver == null || !sDebugLogSaver.isCurrent())) { - // We were not capturing logs before. We are now. - sDebugLogSaver = LogSaver.newInstance(); - } else if (!sCaptureDebugLogs && sDebugLogSaver != null) { - // We were capturing logs. We aren't anymore. - sDebugLogSaver = null; - } - } - - // This is called from FactoryImpl once the Gservices class is initialized. - public static void initializeGservices (final BugleGservices gservices) { - gservices.registerForChanges(new Runnable() { - @Override - public void run() { - refreshGservices(gservices); - } - }); - refreshGservices(gservices); - } - - /** - * Send a {@link #VERBOSE} log message. - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - */ - public static void v(final String tag, final String msg) { - println(android.util.Log.VERBOSE, tag, msg); - } - - /** - * Send a {@link #VERBOSE} log message. - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - * @param tr An exception to log - */ - public static void v(final String tag, final String msg, final Throwable tr) { - println(android.util.Log.VERBOSE, tag, msg + '\n' - + android.util.Log.getStackTraceString(tr)); - } - - /** - * Send a {@link #DEBUG} log message. - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - */ - public static void d(final String tag, final String msg) { - println(android.util.Log.DEBUG, tag, msg); - } - - /** - * Send a {@link #DEBUG} log message and log the exception. - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - * @param tr An exception to log - */ - public static void d(final String tag, final String msg, final Throwable tr) { - println(android.util.Log.DEBUG, tag, msg + '\n' - + android.util.Log.getStackTraceString(tr)); - } - - /** - * Send an {@link #INFO} log message. - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - */ - public static void i(final String tag, final String msg) { - println(android.util.Log.INFO, tag, msg); - } - - /** - * Send a {@link #INFO} log message and log the exception. - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - * @param tr An exception to log - */ - public static void i(final String tag, final String msg, final Throwable tr) { - println(android.util.Log.INFO, tag, msg + '\n' - + android.util.Log.getStackTraceString(tr)); - } - - /** - * Send a {@link #WARN} log message. - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - */ - public static void w(final String tag, final String msg) { - println(android.util.Log.WARN, tag, msg); - } - - /** - * Send a {@link #WARN} log message and log the exception. - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - * @param tr An exception to log - */ - public static void w(final String tag, final String msg, final Throwable tr) { - println(android.util.Log.WARN, tag, msg); - println(android.util.Log.WARN, tag, android.util.Log.getStackTraceString(tr)); - } - - /** - * Send an {@link #ERROR} log message. - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - */ - public static void e(final String tag, final String msg) { - println(android.util.Log.ERROR, tag, msg); - } - - /** - * Send a {@link #ERROR} log message and log the exception. - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - * @param tr An exception to log - */ - public static void e(final String tag, final String msg, final Throwable tr) { - println(android.util.Log.ERROR, tag, msg); - println(android.util.Log.ERROR, tag, android.util.Log.getStackTraceString(tr)); - } - - /** - * What a Terrible Failure: Report a condition that should never happen. - * The error will always be logged at level ASSERT with the call stack. - * Depending on system configuration, a report may be added to the - * {@link android.os.DropBoxManager} and/or the process may be terminated - * immediately with an error dialog. - * @param tag Used to identify the source of a log message. - * @param msg The message you would like logged. - */ - public static void wtf(final String tag, final String msg) { - // Make sure this goes into our log buffer - println(android.util.Log.ASSERT, tag, "wtf\n" + msg); - android.util.Log.wtf(tag, msg, new Exception()); - } - - /** - * What a Terrible Failure: Report a condition that should never happen. - * The error will always be logged at level ASSERT with the call stack. - * Depending on system configuration, a report may be added to the - * {@link android.os.DropBoxManager} and/or the process may be terminated - * immediately with an error dialog. - * @param tag Used to identify the source of a log message. - * @param msg The message you would like logged. - * @param tr An exception to log - */ - public static void wtf(final String tag, final String msg, final Throwable tr) { - // Make sure this goes into our log buffer - println(android.util.Log.ASSERT, tag, "wtf\n" + msg + '\n' + - android.util.Log.getStackTraceString(tr)); - android.util.Log.wtf(tag, msg, tr); - } - - /** - * Low-level logging call. - * @param level The priority/type of this log message - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - */ - private static void println(final int level, final String tag, final String msg) { - android.util.Log.println(level, tag, msg); - - LogSaver serviceLog = sDebugLogSaver; - if (serviceLog != null && level >= android.util.Log.DEBUG) { - serviceLog.log(level, tag, msg); - } - } - - /** - * Save logging into LogSaver only, for dumping to bug report - * - * @param level The priority/type of this log message - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - */ - public static void save(final int level, final String tag, final String msg) { - LogSaver serviceLog = sDebugLogSaver; - if (serviceLog != null) { - serviceLog.log(level, tag, msg); - } - } - - /** - * Checks to see whether or not a log for the specified tag is loggable at the specified level. - * See {@link android.util.Log#isLoggable(String, int)} for more discussion. - */ - public static boolean isLoggable(final String tag, final int level) { - return android.util.Log.isLoggable(tag, level); - } - - /** - * Returns text as is if {@value #BUGLE_TAG}'s log level is set to DEBUG or VERBOSE; - * returns "--" otherwise. Useful for log statements where we don't want to log - * various strings (e.g., usernames) with default logging to avoid leaking PII in logcat. - */ - public static String sanitizePII(final String text) { - if (text == null) { - return null; - } - - if (android.util.Log.isLoggable(BUGLE_TAG, android.util.Log.DEBUG)) { - return text; - } else { - return "Redacted-" + text.length(); - } - } - - public static void dump(java.io.PrintWriter out) { - final LogSaver logsaver = sDebugLogSaver; - if (logsaver != null) { - logsaver.dump(out); - } - } -} diff --git a/src/com/android/messaging/util/LoggingTimer.java b/src/com/android/messaging/util/LoggingTimer.java deleted file mode 100644 index d0d41ac..0000000 --- a/src/com/android/messaging/util/LoggingTimer.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.os.SystemClock; - -/** - * A utility timer that logs the execution time of operations - */ -public class LoggingTimer { - private static final int NO_WARN_LIMIT = -1; - - private final String mTag; - private final String mName; - private final long mWarnLimitMillis; - private long mStartMillis; - - public LoggingTimer(final String tag, final String name) { - this(tag, name, NO_WARN_LIMIT); - } - - public LoggingTimer(final String tag, final String name, final long warnLimitMillis) { - mTag = tag; - mName = name; - mWarnLimitMillis = warnLimitMillis; - } - - /** - * This method should be called at the start of the operation to be timed. - */ - public void start() { - mStartMillis = SystemClock.elapsedRealtime(); - - if (LogUtil.isLoggable(mTag, LogUtil.VERBOSE)) { - LogUtil.v(mTag, "Timer start for " + mName); - } - } - - /** - * This method should be called at the end of the operation to be timed. It logs the time since - * the last call to {@link #start} - */ - public void stopAndLog() { - final long elapsedMs = SystemClock.elapsedRealtime() - mStartMillis; - - final String logMessage = String.format("Used %dms for %s", elapsedMs, mName); - - LogUtil.save(LogUtil.DEBUG, mTag, logMessage); - - if (mWarnLimitMillis != NO_WARN_LIMIT && elapsedMs > mWarnLimitMillis) { - LogUtil.w(mTag, logMessage); - } else if (LogUtil.isLoggable(mTag, LogUtil.VERBOSE)) { - LogUtil.v(mTag, logMessage); - } - } -} diff --git a/src/com/android/messaging/util/LongSparseSet.java b/src/com/android/messaging/util/LongSparseSet.java deleted file mode 100644 index 8e2cfca..0000000 --- a/src/com/android/messaging/util/LongSparseSet.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.support.v4.util.LongSparseArray; - -/** - * A space saving set for long values using v4 compat LongSparseArray - */ -public class LongSparseSet { - private static final Object THE_ONLY_VALID_VALUE = new Object(); - private final LongSparseArray<Object> mSet = new LongSparseArray<Object>(); - - public LongSparseSet() { - } - - /** - * @param key The element to check - * @return True if the element is in the set, false otherwise - */ - public boolean contains(long key) { - if (mSet.get(key, null/*default*/) == THE_ONLY_VALID_VALUE) { - return true; - } - return false; - } - - /** - * Add an element to the set - * - * @param key The element to add - */ - public void add(long key) { - mSet.put(key, THE_ONLY_VALID_VALUE); - } - - /** - * Remove an element from the set - * - * @param key The element to remove - */ - public void remove(long key) { - mSet.delete(key); - } -} diff --git a/src/com/android/messaging/util/MaterialPalette.java b/src/com/android/messaging/util/MaterialPalette.java deleted file mode 100644 index 8dbacbf..0000000 --- a/src/com/android/messaging/util/MaterialPalette.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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.android.messaging.util; - - -public class MaterialPalette{ - public final int mPrimaryColor; - public final int mSecondaryColor; - - public MaterialPalette(final int primaryColor, final int secondaryColor) { - mPrimaryColor = primaryColor; - mSecondaryColor = secondaryColor; - } -} diff --git a/src/com/android/messaging/util/MediaMetadataRetrieverWrapper.java b/src/com/android/messaging/util/MediaMetadataRetrieverWrapper.java deleted file mode 100644 index b1078d1..0000000 --- a/src/com/android/messaging/util/MediaMetadataRetrieverWrapper.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.ContentResolver; -import android.content.res.AssetFileDescriptor; -import android.graphics.Bitmap; -import android.media.MediaMetadataRetriever; -import android.net.Uri; -import android.text.TextUtils; - -import com.android.messaging.Factory; - -import java.io.IOException; - -/** - * Convenience wrapper for {@link MediaMetadataRetriever} to help with its eccentric error handling. - */ -public class MediaMetadataRetrieverWrapper { - private final MediaMetadataRetriever mRetriever = new MediaMetadataRetriever(); - - public MediaMetadataRetrieverWrapper() { - } - - public void setDataSource(Uri uri) throws IOException { - ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); - AssetFileDescriptor fd = resolver.openAssetFileDescriptor(uri, "r"); - if (fd == null) { - throw new IOException("openAssetFileDescriptor returned null for " + uri); - } - try { - mRetriever.setDataSource(fd.getFileDescriptor()); - } catch (RuntimeException e) { - release(); - throw new IOException(e); - } finally { - fd.close(); - } - } - - public int extractInteger(final int key, final int defaultValue) { - final String s = mRetriever.extractMetadata(key); - if (TextUtils.isEmpty(s)) { - return defaultValue; - } - return Integer.parseInt(s); - } - - public String extractMetadata(final int key) { - return mRetriever.extractMetadata(key); - } - - public Bitmap getFrameAtTime() { - return mRetriever.getFrameAtTime(); - } - - public Bitmap getFrameAtTime(final long timeUs) { - return mRetriever.getFrameAtTime(timeUs); - } - - public void release() { - try { - mRetriever.release(); - } catch (RuntimeException e) { - LogUtil.e(LogUtil.BUGLE_TAG, "MediaMetadataRetriever.release failed", e); - } - } -} diff --git a/src/com/android/messaging/util/MediaUtil.java b/src/com/android/messaging/util/MediaUtil.java deleted file mode 100644 index f25354c..0000000 --- a/src/com/android/messaging/util/MediaUtil.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; - -import com.android.messaging.Factory; - -public abstract class MediaUtil { - public static interface OnCompletionListener { - public void onCompletion(); - } - - public static MediaUtil get() { - return Factory.get().getMediaUtil(); - } - - /** - * Play sound from local resources given a resource id. - */ - public abstract void playSound(final Context context, final int resId, - final OnCompletionListener completionListener); -} diff --git a/src/com/android/messaging/util/MediaUtilImpl.java b/src/com/android/messaging/util/MediaUtilImpl.java deleted file mode 100644 index 272a057..0000000 --- a/src/com/android/messaging/util/MediaUtilImpl.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; -import android.content.res.AssetFileDescriptor; -import android.media.AudioManager; -import android.media.MediaPlayer; - -/** - * Default implementation of MediaUtil - */ -public class MediaUtilImpl extends MediaUtil { - - @Override - public void playSound(final Context context, final int resId, - final OnCompletionListener completionListener) { - // We want to play at the media volume and not the ringer volume, but we do want to - // avoid playing sound when the ringer/notifications are silenced. This is used for - // in app sounds that are not critical and should not impact running silent but also - // shouldn't play at full ring volume if you want to hear your ringer but don't want - // to be annoyed with in-app volume. - AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - try { - final MediaPlayer mediaPlayer = new MediaPlayer(); - mediaPlayer.setAudioStreamType(AudioManager.STREAM_NOTIFICATION); - final AssetFileDescriptor afd = context.getResources().openRawResourceFd(resId); - mediaPlayer.setDataSource( - afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); - afd.close(); - mediaPlayer.prepare(); - mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(final MediaPlayer mp) { - if (completionListener != null) { - completionListener.onCompletion(); - } - mp.stop(); - mp.release(); - } - }); - mediaPlayer.seekTo(0); - mediaPlayer.start(); - return; - } catch (final Exception e) { - LogUtil.w("MediaUtilImpl", "Error playing sound id: " + resId, e); - } - if (completionListener != null) { - // Call the completion handler to not block functionality if audio play fails - completionListener.onCompletion(); - } - } -}
\ No newline at end of file diff --git a/src/com/android/messaging/util/NotificationPlayer.java b/src/com/android/messaging/util/NotificationPlayer.java deleted file mode 100644 index a4ed44e..0000000 --- a/src/com/android/messaging/util/NotificationPlayer.java +++ /dev/null @@ -1,363 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; -import android.media.AudioManager; -import android.media.MediaPlayer; -import android.media.MediaPlayer.OnCompletionListener; -import android.net.Uri; -import android.os.Looper; -import android.os.PowerManager; -import android.os.SystemClock; - -import com.android.messaging.Factory; - -import java.util.LinkedList; - -/** - * This class is provides the same interface and functionality as android.media.AsyncPlayer - * with the following differences: - * - whenever audio is played, audio focus is requested, - * - whenever audio playback is stopped or the playback completed, audio focus is abandoned. - * - * This file has been copied from com.android.server.NotificationPlayer. The only modification is - * the addition of a volume parameter. Hopefully the framework will adapt AsyncPlayer to support - * all the functionality in this class, at which point this one can be deleted. - */ -public class NotificationPlayer implements OnCompletionListener { - private static final int PLAY = 1; - private static final int STOP = 2; - private static final boolean mDebug = false; - - private static final class Command { - int code; - Uri uri; - boolean looping; - int stream; - float volume; - long requestTime; - boolean releaseFocus; - - @Override - public String toString() { - return "{ code=" + code + " looping=" + looping + " stream=" + stream - + " uri=" + uri + " }"; - } - } - - private final LinkedList<Command> mCmdQueue = new LinkedList<Command>(); - - private Looper mLooper; - - /* - * Besides the use of audio focus, the only implementation difference between AsyncPlayer and - * NotificationPlayer resides in the creation of the MediaPlayer. For the completion callback, - * OnCompletionListener, to be called at the end of the playback, the MediaPlayer needs to - * be created with a looper running so its event handler is not null. - */ - private final class CreationAndCompletionThread extends Thread { - public Command mCmd; - public CreationAndCompletionThread(final Command cmd) { - super(); - mCmd = cmd; - } - - @Override - public void run() { - Looper.prepare(); - mLooper = Looper.myLooper(); - synchronized (this) { - final AudioManager audioManager = - (AudioManager) Factory.get().getApplicationContext() - .getSystemService(Context.AUDIO_SERVICE); - try { - final MediaPlayer player = new MediaPlayer(); - player.setAudioStreamType(mCmd.stream); - player.setDataSource(Factory.get().getApplicationContext(), mCmd.uri); - player.setLooping(mCmd.looping); - player.setVolume(mCmd.volume, mCmd.volume); - player.prepare(); - if ((mCmd.uri != null) && (mCmd.uri.getEncodedPath() != null) - && (mCmd.uri.getEncodedPath().length() > 0)) { - audioManager.requestAudioFocus(null, mCmd.stream, - mCmd.looping ? AudioManager.AUDIOFOCUS_GAIN_TRANSIENT - : AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); - } - player.setOnCompletionListener(NotificationPlayer.this); - player.start(); - if (mPlayer != null) { - mPlayer.release(); - } - mPlayer = player; - } catch (final Exception e) { - LogUtil.w(mTag, "error loading sound for " + mCmd.uri, e); - } - mAudioManager = audioManager; - this.notify(); - } - Looper.loop(); - } - } - - private void startSound(final Command cmd) { - // Preparing can be slow, so if there is something else - // is playing, let it continue until we're done, so there - // is less of a glitch. - try { - if (mDebug) { - LogUtil.d(mTag, "Starting playback"); - } - //----------------------------------- - // This is were we deviate from the AsyncPlayer implementation and create the - // MediaPlayer in a new thread with which we're synchronized - synchronized (mCompletionHandlingLock) { - // if another sound was already playing, it doesn't matter we won't get notified - // of the completion, since only the completion notification of the last sound - // matters - if ((mLooper != null) - && (mLooper.getThread().getState() != Thread.State.TERMINATED)) { - mLooper.quit(); - } - mCompletionThread = new CreationAndCompletionThread(cmd); - synchronized (mCompletionThread) { - mCompletionThread.start(); - mCompletionThread.wait(); - } - } - //----------------------------------- - - final long delay = SystemClock.elapsedRealtime() - cmd.requestTime; - if (delay > 1000) { - LogUtil.w(mTag, "Notification sound delayed by " + delay + "msecs"); - } - } catch (final Exception e) { - LogUtil.w(mTag, "error loading sound for " + cmd.uri, e); - } - } - - private void stopSound(final Command cmd) { - if (mPlayer == null) { - return; - } - final long delay = SystemClock.elapsedRealtime() - cmd.requestTime; - if (delay > 1000) { - LogUtil.w(mTag, "Notification stop delayed by " + delay + "msecs"); - } - mPlayer.stop(); - mPlayer.release(); - mPlayer = null; - if (cmd.releaseFocus && mAudioManager != null) { - mAudioManager.abandonAudioFocus(null); - } - mAudioManager = null; - if ((mLooper != null) && (mLooper.getThread().getState() != Thread.State.TERMINATED)) { - mLooper.quit(); - } - } - - private final class CmdThread extends java.lang.Thread { - CmdThread() { - super("NotificationPlayer-" + mTag); - } - - @Override - public void run() { - while (true) { - Command cmd = null; - - synchronized (mCmdQueue) { - if (mDebug) { - LogUtil.d(mTag, "RemoveFirst"); - } - cmd = mCmdQueue.removeFirst(); - } - - switch (cmd.code) { - case PLAY: - if (mDebug) { - LogUtil.d(mTag, "PLAY"); - } - startSound(cmd); - break; - case STOP: - if (mDebug) { - LogUtil.d(mTag, "STOP"); - } - stopSound(cmd); - break; - } - - synchronized (mCmdQueue) { - if (mCmdQueue.size() == 0) { - // nothing left to do, quit - // doing this check after we're done prevents the case where they - // added it during the operation from spawning two threads and - // trying to do them in parallel. - mThread = null; - releaseWakeLock(); - return; - } - } - } - } - } - - @Override - public void onCompletion(final MediaPlayer mp) { - if (mAudioManager != null) { - mAudioManager.abandonAudioFocus(null); - } - // if there are no more sounds to play, end the Looper to listen for media completion - synchronized (mCmdQueue) { - if (mCmdQueue.size() == 0) { - synchronized (mCompletionHandlingLock) { - if (mLooper != null) { - mLooper.quit(); - } - mCompletionThread = null; - } - } - } - } - - private String mTag; - private CmdThread mThread; - private CreationAndCompletionThread mCompletionThread; - private final Object mCompletionHandlingLock = new Object(); - private MediaPlayer mPlayer; - private PowerManager.WakeLock mWakeLock; - private AudioManager mAudioManager; - - // The current state according to the caller. Reality lags behind - // because of the asynchronous nature of this class. - private int mState = STOP; - - /** - * Construct a NotificationPlayer object. - * - * @param tag a string to use for debugging - */ - public NotificationPlayer(final String tag) { - if (tag != null) { - mTag = tag; - } else { - mTag = "NotificationPlayer"; - } - } - - /** - * Start playing the sound. It will actually start playing at some - * point in the future. There are no guarantees about latency here. - * Calling this before another audio file is done playing will stop - * that one and start the new one. - * - * @param uri The URI to play. (see {@link MediaPlayer#setDataSource(Context, Uri)}) - * @param looping Whether the audio should loop forever. - * (see {@link MediaPlayer#setLooping(boolean)}) - * @param stream the AudioStream to use. - * (see {@link MediaPlayer#setAudioStreamType(int)}) - * @param volume The volume at which to play this sound, as a fraction of the system volume for - * the relevant stream type. A value of 1 is the maximum and means play at the system - * volume with no attenuation. - */ - public void play(final Uri uri, final boolean looping, final int stream, final float volume) { - final Command cmd = new Command(); - cmd.requestTime = SystemClock.elapsedRealtime(); - cmd.code = PLAY; - cmd.uri = uri; - cmd.looping = looping; - cmd.stream = stream; - cmd.volume = volume; - synchronized (mCmdQueue) { - enqueueLocked(cmd); - mState = PLAY; - } - } - - /** Same as calling stop(true) */ - public void stop() { - stop(true); - } - - /** - * Stop a previously played sound. It can't be played again or unpaused - * at this point. Calling this multiple times has no ill effects. - * @param releaseAudioFocus whether to release audio focus - */ - public void stop(final boolean releaseAudioFocus) { - synchronized (mCmdQueue) { - // This check allows stop to be called multiple times without starting - // a thread that ends up doing nothing. - if (mState != STOP) { - final Command cmd = new Command(); - cmd.requestTime = SystemClock.elapsedRealtime(); - cmd.code = STOP; - cmd.releaseFocus = releaseAudioFocus; - enqueueLocked(cmd); - mState = STOP; - } - } - } - - private void enqueueLocked(final Command cmd) { - mCmdQueue.add(cmd); - if (mThread == null) { - acquireWakeLock(); - mThread = new CmdThread(); - mThread.start(); - } - } - - /** - * We want to hold a wake lock while we do the prepare and play. The stop probably is - * optional, but it won't hurt to have it too. The problem is that if you start a sound - * while you're holding a wake lock (e.g. an alarm starting a notification), you want the - * sound to play, but if the CPU turns off before mThread gets to work, it won't. The - * simplest way to deal with this is to make it so there is a wake lock held while the - * thread is starting or running. You're going to need the WAKE_LOCK permission if you're - * going to call this. - * - * This must be called before the first time play is called. - * - * @hide - */ - public void setUsesWakeLock() { - if (mWakeLock != null || mThread != null) { - // if either of these has happened, we've already played something. - // and our releases will be out of sync. - throw new RuntimeException("assertion failed mWakeLock=" + mWakeLock - + " mThread=" + mThread); - } - final PowerManager pm = (PowerManager) Factory.get().getApplicationContext() - .getSystemService(Context.POWER_SERVICE); - mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag); - } - - private void acquireWakeLock() { - if (mWakeLock != null) { - mWakeLock.acquire(); - } - } - - private void releaseWakeLock() { - if (mWakeLock != null) { - mWakeLock.release(); - } - } -} - diff --git a/src/com/android/messaging/util/OsUtil.java b/src/com/android/messaging/util/OsUtil.java deleted file mode 100644 index e45a63c..0000000 --- a/src/com/android/messaging/util/OsUtil.java +++ /dev/null @@ -1,269 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.Manifest; -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.UserHandle; -import android.os.UserManager; - -import com.android.messaging.Factory; - -import java.util.ArrayList; -import java.util.Hashtable; -import java.util.Set; - -/** - * Android OS version utilities - */ -public class OsUtil { - private static boolean sIsAtLeastICS_MR1; - private static boolean sIsAtLeastJB; - private static boolean sIsAtLeastJB_MR1; - private static boolean sIsAtLeastJB_MR2; - private static boolean sIsAtLeastKLP; - private static boolean sIsAtLeastL; - private static boolean sIsAtLeastL_MR1; - private static boolean sIsAtLeastM; - - private static Boolean sIsSecondaryUser = null; - - static { - final int v = getApiVersion(); - sIsAtLeastICS_MR1 = v >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1; - sIsAtLeastJB = v >= android.os.Build.VERSION_CODES.JELLY_BEAN; - sIsAtLeastJB_MR1 = v >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; - sIsAtLeastJB_MR2 = v >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; - sIsAtLeastKLP = v >= android.os.Build.VERSION_CODES.KITKAT; - sIsAtLeastL = v >= android.os.Build.VERSION_CODES.LOLLIPOP; - sIsAtLeastL_MR1 = v >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1; - sIsAtLeastM = v >= android.os.Build.VERSION_CODES.M; - } - - /** - * @return True if the version of Android that we're running on is at least Ice Cream Sandwich - * MR1 (API level 15). - */ - public static boolean isAtLeastICS_MR1() { - return sIsAtLeastICS_MR1; - } - - /** - * @return True if the version of Android that we're running on is at least Jelly Bean - * (API level 16). - */ - public static boolean isAtLeastJB() { - return sIsAtLeastJB; - } - - /** - * @return True if the version of Android that we're running on is at least Jelly Bean MR1 - * (API level 17). - */ - public static boolean isAtLeastJB_MR1() { - return sIsAtLeastJB_MR1; - } - - /** - * @return True if the version of Android that we're running on is at least Jelly Bean MR2 - * (API level 18). - */ - public static boolean isAtLeastJB_MR2() { - return sIsAtLeastJB_MR2; - } - - /** - * @return True if the version of Android that we're running on is at least KLP - * (API level 19). - */ - public static boolean isAtLeastKLP() { - return sIsAtLeastKLP; - } - - /** - * @return True if the version of Android that we're running on is at least L - * (API level 21). - */ - public static boolean isAtLeastL() { - return sIsAtLeastL; - } - - /** - * @return True if the version of Android that we're running on is at least L MR1 - * (API level 22). - */ - public static boolean isAtLeastL_MR1() { - return sIsAtLeastL_MR1; - } - - /** - * @return True if the version of Android that we're running on is at least M - * (API level 23). - */ - public static boolean isAtLeastM() { - return sIsAtLeastM; - } - - /** - * @return The Android API version of the OS that we're currently running on. - */ - public static int getApiVersion() { - return android.os.Build.VERSION.SDK_INT; - } - - public static boolean isSecondaryUser() { - if (sIsSecondaryUser == null) { - final Context context = Factory.get().getApplicationContext(); - boolean isSecondaryUser = false; - - // Only check for newer devices (but not the nexus 10) - if (OsUtil.sIsAtLeastJB_MR1 && !"Nexus 10".equals(Build.MODEL)) { - final UserHandle uh = android.os.Process.myUserHandle(); - final UserManager userManager = - (UserManager) context.getSystemService(Context.USER_SERVICE); - if (userManager != null) { - final long userSerialNumber = userManager.getSerialNumberForUser(uh); - isSecondaryUser = (0 != userSerialNumber); - } - } - sIsSecondaryUser = isSecondaryUser; - } - return sIsSecondaryUser; - } - - /** - * Creates a joined string from a Set<String> using the given delimiter. - * @param values - * @param delimiter - * @return - */ - public static String joinFromSetWithDelimiter( - final Set<String> values, final String delimiter) { - if (values != null) { - final StringBuilder joinedStringBuilder = new StringBuilder(); - boolean firstValue = true; - for (final String value : values) { - if (firstValue) { - firstValue = false; - } else { - joinedStringBuilder.append(delimiter); - } - joinedStringBuilder.append(value); - } - return joinedStringBuilder.toString(); - } - return null; - } - - private static Hashtable<String, Integer> sPermissions = new Hashtable<String, Integer>(); - - /** - * Check if the app has the specified permission. If it does not, the app needs to use - * {@link android.app.Activity#requestPermission}. Note that if it - * returns true, it cannot return false in the same process as the OS kills the process when - * any permission is revoked. - * @param permission A permission from {@link android.Manifest.permission} - */ - public static boolean hasPermission(final String permission) { - if (OsUtil.isAtLeastM()) { - // It is safe to cache the PERMISSION_GRANTED result as the process gets killed if the - // user revokes the permission setting. However, PERMISSION_DENIED should not be - // cached as the process does not get killed if the user enables the permission setting. - if (!sPermissions.containsKey(permission) - || sPermissions.get(permission) == PackageManager.PERMISSION_DENIED) { - final Context context = Factory.get().getApplicationContext(); - final int permissionState = context.checkSelfPermission(permission); - sPermissions.put(permission, permissionState); - } - return sPermissions.get(permission) == PackageManager.PERMISSION_GRANTED; - } else { - return true; - } - } - - /** Does the app have all the specified permissions */ - public static boolean hasPermissions(final String[] permissions) { - for (final String permission : permissions) { - if (!hasPermission(permission)) { - return false; - } - } - return true; - } - - public static boolean hasPhonePermission() { - return hasPermission(Manifest.permission.READ_PHONE_STATE); - } - - public static boolean hasSmsPermission() { - return hasPermission(Manifest.permission.READ_SMS); - } - - public static boolean hasLocationPermission() { - return OsUtil.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION); - } - - - public static boolean hasStoragePermission() { - // Note that READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE are granted or denied - // together. - return OsUtil.hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE); - } - - public static boolean hasRecordAudioPermission() { - return OsUtil.hasPermission(Manifest.permission.RECORD_AUDIO); - } - - /** - * Returns array with the set of permissions that have not been granted from the given set. - * The array will be empty if the app has all of the specified permissions. Note that calling - * {@link Activity#requestPermissions} for an already granted permission can prompt the user - * again, and its up to the app to only request permissions that are missing. - */ - public static String[] getMissingPermissions(final String[] permissions) { - final ArrayList<String> missingList = new ArrayList<String>(); - for (final String permission : permissions) { - if (!hasPermission(permission)) { - missingList.add(permission); - } - } - - final String[] missingArray = new String[missingList.size()]; - missingList.toArray(missingArray); - return missingArray; - } - - private static String[] sRequiredPermissions = new String[] { - // Required to read existing SMS threads - Manifest.permission.READ_SMS, - // Required for knowing the phone number, number of SIMs, etc. - Manifest.permission.READ_PHONE_STATE, - // This is not strictly required, but simplifies the contact picker scenarios - Manifest.permission.READ_CONTACTS, - }; - - /** Does the app have the minimum set of permissions required to operate. */ - public static boolean hasRequiredPermissions() { - return hasPermissions(sRequiredPermissions); - } - - public static String[] getMissingRequiredPermissions() { - return getMissingPermissions(sRequiredPermissions); - } -} diff --git a/src/com/android/messaging/util/PendingIntentConstants.java b/src/com/android/messaging/util/PendingIntentConstants.java deleted file mode 100644 index 1a594c5..0000000 --- a/src/com/android/messaging/util/PendingIntentConstants.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.android.messaging.util; - - -public class PendingIntentConstants { - // Notifications - public static final int SMS_NOTIFICATION_ID = 0; - public static final int SMS_SECONDARY_USER_NOTIFICATION_ID = 1; - public static final int MSG_SEND_ERROR = 2; - public static final int SMS_STORAGE_LOW_NOTIFICATION_ID = 3; - - // Request codes - public static final int UPDATE_NOTIFICATIONS_ALARM_ACTION_ID = 100; - - public static final int MIN_ASSIGNED_REQUEST_CODE = 1001; - - // Logging - private static final String TAG = LogUtil.BUGLE_TAG; - private static final boolean VERBOSE = false; - - // Internal Constants - private static final String NOTIFICATION_REQUEST_CODE_PREFS = "notificationRequestCodes.v1"; - private static final String REQUEST_CODE_DELIMITER = "|"; - private static final String MAX_REQUEST_CODE_KEY = "maxRequestCode"; -} diff --git a/src/com/android/messaging/util/PhoneUtils.java b/src/com/android/messaging/util/PhoneUtils.java deleted file mode 100644 index 726f083..0000000 --- a/src/com/android/messaging/util/PhoneUtils.java +++ /dev/null @@ -1,1011 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.database.Cursor; -import android.net.ConnectivityManager; -import android.provider.Settings; -import android.provider.Telephony; -import android.support.v4.util.ArrayMap; -import android.telephony.PhoneNumberUtils; -import android.telephony.SmsManager; -import android.telephony.SubscriptionInfo; -import android.telephony.SubscriptionManager; -import android.telephony.TelephonyManager; -import android.text.TextUtils; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.sms.MmsSmsUtils; -import com.google.i18n.phonenumbers.NumberParseException; -import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; -import com.google.i18n.phonenumbers.PhoneNumberUtil; -import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; - -/** - * This class abstracts away platform dependency of calling telephony related - * platform APIs, mostly involving TelephonyManager, SubscriptionManager and - * a bit of SmsManager. - * - * The class instance can only be obtained via the get(int subId) method parameterized - * by a SIM subscription ID. On pre-L_MR1, the subId is not used and it has to be - * the default subId (-1). - * - * A convenient getDefault() method is provided for default subId (-1) on any platform - */ -public abstract class PhoneUtils { - private static final String TAG = LogUtil.BUGLE_TAG; - - private static final int MINIMUM_PHONE_NUMBER_LENGTH_TO_FORMAT = 6; - - private static final List<SubscriptionInfo> EMPTY_SUBSCRIPTION_LIST = new ArrayList<>(); - - // The canonical phone number cache - // Each country gets its own cache. The following maps from ISO country code to - // the country's cache. Each cache maps from original phone number to canonicalized phone - private static final ArrayMap<String, ArrayMap<String, String>> sCanonicalPhoneNumberCache = - new ArrayMap<>(); - - protected final Context mContext; - protected final TelephonyManager mTelephonyManager; - protected final int mSubId; - - public PhoneUtils(int subId) { - mSubId = subId; - mContext = Factory.get().getApplicationContext(); - mTelephonyManager = - (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); - } - - /** - * Get the SIM's country code - * - * @return the country code on the SIM - */ - public abstract String getSimCountry(); - - /** - * Get number of SIM slots - * - * @return the SIM slot count - */ - public abstract int getSimSlotCount(); - - /** - * Get SIM's carrier name - * - * @return the carrier name of the SIM - */ - public abstract String getCarrierName(); - - /** - * Check if there is SIM inserted on the device - * - * @return true if there is SIM inserted, false otherwise - */ - public abstract boolean hasSim(); - - /** - * Check if the SIM is roaming - * - * @return true if the SIM is in romaing state, false otherwise - */ - public abstract boolean isRoaming(); - - /** - * Get the MCC and MNC in integer of the SIM's provider - * - * @return an array of two ints, [0] is the MCC code and [1] is the MNC code - */ - public abstract int[] getMccMnc(); - - /** - * Get the mcc/mnc string - * - * @return the text of mccmnc string - */ - public abstract String getSimOperatorNumeric(); - - /** - * Get the SIM's self raw number, i.e. not canonicalized - * - * @param allowOverride Whether to use the app's setting to override the self number - * @return the original self number - * @throws IllegalStateException if no active subscription on L-MR1+ - */ - public abstract String getSelfRawNumber(final boolean allowOverride); - - /** - * Returns the "effective" subId, or the subId used in the context of actual messages, - * conversations and subscription-specific settings, for the given "nominal" sub id. - * - * For pre-L-MR1 platform, this should always be - * {@value com.android.messaging.datamodel.data.ParticipantData#DEFAULT_SELF_SUB_ID}; - * - * On the other hand, for L-MR1 and above, DEFAULT_SELF_SUB_ID will be mapped to the system - * default subscription id for SMS. - * - * @param subId The input subId - * @return the real subId if we can convert - */ - public abstract int getEffectiveSubId(int subId); - - /** - * Returns the number of active subscriptions in the device. - */ - public abstract int getActiveSubscriptionCount(); - - /** - * Get {@link SmsManager} instance - * - * @return the relevant SmsManager instance based on OS version and subId - */ - public abstract SmsManager getSmsManager(); - - /** - * Get the default SMS subscription id - * - * @return the default sub ID - */ - public abstract int getDefaultSmsSubscriptionId(); - - /** - * Returns if there's currently a system default SIM selected for sending SMS. - */ - public abstract boolean getHasPreferredSmsSim(); - - /** - * For L_MR1, system may return a negative subId. Convert this into our own - * subId, so that we consistently use -1 for invalid or default. - * - * see b/18629526 and b/18670346 - * - * @param intent The push intent from system - * @param extraName The name of the sub id extra - * @return the subId that is valid and meaningful for the app - */ - public abstract int getEffectiveIncomingSubIdFromSystem(Intent intent, String extraName); - - /** - * Get the subscription_id column value from a telephony provider cursor - * - * @param cursor The database query cursor - * @param subIdIndex The index of the subId column in the cursor - * @return the subscription_id column value from the cursor - */ - public abstract int getSubIdFromTelephony(Cursor cursor, int subIdIndex); - - /** - * Check if data roaming is enabled - * - * @return true if data roaming is enabled, false otherwise - */ - public abstract boolean isDataRoamingEnabled(); - - /** - * Check if mobile data is enabled - * - * @return true if mobile data is enabled, false otherwise - */ - public abstract boolean isMobileDataEnabled(); - - /** - * Get the set of self phone numbers, all normalized - * - * @return the set of normalized self phone numbers - */ - public abstract HashSet<String> getNormalizedSelfNumbers(); - - /** - * This interface packages methods should only compile on L_MR1. - * This is needed to make unit tests happy when mockito tries to - * mock these methods. Calling on these methods on L_MR1 requires - * an extra invocation of toMr1(). - */ - public interface LMr1 { - /** - * Get this SIM's information. Only applies to L_MR1 above - * - * @return the subscription info of the SIM - */ - public abstract SubscriptionInfo getActiveSubscriptionInfo(); - - /** - * Get the list of active SIMs in system. Only applies to L_MR1 above - * - * @return the list of subscription info for all inserted SIMs - */ - public abstract List<SubscriptionInfo> getActiveSubscriptionInfoList(); - - /** - * Register subscription change listener. Only applies to L_MR1 above - * - * @param listener The listener to register - */ - public abstract void registerOnSubscriptionsChangedListener( - SubscriptionManager.OnSubscriptionsChangedListener listener); - } - - /** - * The PhoneUtils class for pre L_MR1 - */ - public static class PhoneUtilsPreLMR1 extends PhoneUtils { - private final ConnectivityManager mConnectivityManager; - - public PhoneUtilsPreLMR1() { - super(ParticipantData.DEFAULT_SELF_SUB_ID); - mConnectivityManager = - (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - } - - @Override - public String getSimCountry() { - final String country = mTelephonyManager.getSimCountryIso(); - if (TextUtils.isEmpty(country)) { - return null; - } - return country.toUpperCase(); - } - - @Override - public int getSimSlotCount() { - // Don't support MSIM pre-L_MR1 - return 1; - } - - @Override - public String getCarrierName() { - return mTelephonyManager.getNetworkOperatorName(); - } - - @Override - public boolean hasSim() { - return mTelephonyManager.getSimState() != TelephonyManager.SIM_STATE_ABSENT; - } - - @Override - public boolean isRoaming() { - return mTelephonyManager.isNetworkRoaming(); - } - - @Override - public int[] getMccMnc() { - final String mccmnc = mTelephonyManager.getSimOperator(); - int mcc = 0; - int mnc = 0; - try { - mcc = Integer.parseInt(mccmnc.substring(0, 3)); - mnc = Integer.parseInt(mccmnc.substring(3)); - } catch (Exception e) { - LogUtil.w(TAG, "PhoneUtils.getMccMnc: invalid string " + mccmnc, e); - } - return new int[]{mcc, mnc}; - } - - @Override - public String getSimOperatorNumeric() { - return mTelephonyManager.getSimOperator(); - } - - @Override - public String getSelfRawNumber(final boolean allowOverride) { - if (allowOverride) { - final String userDefinedNumber = getNumberFromPrefs(mContext, - ParticipantData.DEFAULT_SELF_SUB_ID); - if (!TextUtils.isEmpty(userDefinedNumber)) { - return userDefinedNumber; - } - } - return mTelephonyManager.getLine1Number(); - } - - @Override - public int getEffectiveSubId(int subId) { - Assert.equals(ParticipantData.DEFAULT_SELF_SUB_ID, subId); - return ParticipantData.DEFAULT_SELF_SUB_ID; - } - - @Override - public SmsManager getSmsManager() { - return SmsManager.getDefault(); - } - - @Override - public int getDefaultSmsSubscriptionId() { - Assert.fail("PhoneUtils.getDefaultSmsSubscriptionId(): not supported before L MR1"); - return ParticipantData.DEFAULT_SELF_SUB_ID; - } - - @Override - public boolean getHasPreferredSmsSim() { - // SIM selection is not supported pre-L_MR1. - return true; - } - - @Override - public int getActiveSubscriptionCount() { - return hasSim() ? 1 : 0; - } - - @Override - public int getEffectiveIncomingSubIdFromSystem(Intent intent, String extraName) { - // Pre-L_MR1 always returns the default id - return ParticipantData.DEFAULT_SELF_SUB_ID; - } - - @Override - public int getSubIdFromTelephony(Cursor cursor, int subIdIndex) { - // No subscription_id column before L_MR1 - return ParticipantData.DEFAULT_SELF_SUB_ID; - } - - @Override - @SuppressWarnings("deprecation") - public boolean isDataRoamingEnabled() { - boolean dataRoamingEnabled = false; - final ContentResolver cr = mContext.getContentResolver(); - if (OsUtil.isAtLeastJB_MR1()) { - dataRoamingEnabled = - (Settings.Global.getInt(cr, Settings.Global.DATA_ROAMING, 0) != 0); - } else { - dataRoamingEnabled = - (Settings.System.getInt(cr, Settings.System.DATA_ROAMING, 0) != 0); - } - return dataRoamingEnabled; - } - - @Override - public boolean isMobileDataEnabled() { - boolean mobileDataEnabled = false; - try { - final Class cmClass = mConnectivityManager.getClass(); - final Method method = cmClass.getDeclaredMethod("getMobileDataEnabled"); - method.setAccessible(true); // Make the method callable - // get the setting for "mobile data" - mobileDataEnabled = (Boolean) method.invoke(mConnectivityManager); - } catch (final Exception e) { - LogUtil.e(TAG, "PhoneUtil.isMobileDataEnabled: system api not found", e); - } - return mobileDataEnabled; - } - - @Override - public HashSet<String> getNormalizedSelfNumbers() { - final HashSet<String> numbers = new HashSet<>(); - numbers.add(getCanonicalForSelf(true/*allowOverride*/)); - return numbers; - } - } - - /** - * The PhoneUtils class for L_MR1 - */ - public static class PhoneUtilsLMR1 extends PhoneUtils implements LMr1 { - private final SubscriptionManager mSubscriptionManager; - - public PhoneUtilsLMR1(final int subId) { - super(subId); - mSubscriptionManager = SubscriptionManager.from(Factory.get().getApplicationContext()); - } - - @Override - public String getSimCountry() { - final SubscriptionInfo subInfo = getActiveSubscriptionInfo(); - if (subInfo != null) { - final String country = subInfo.getCountryIso(); - if (TextUtils.isEmpty(country)) { - return null; - } - return country.toUpperCase(); - } - return null; - } - - @Override - public int getSimSlotCount() { - return mSubscriptionManager.getActiveSubscriptionInfoCountMax(); - } - - @Override - public String getCarrierName() { - final SubscriptionInfo subInfo = getActiveSubscriptionInfo(); - if (subInfo != null) { - final CharSequence displayName = subInfo.getDisplayName(); - if (!TextUtils.isEmpty(displayName)) { - return displayName.toString(); - } - final CharSequence carrierName = subInfo.getCarrierName(); - if (carrierName != null) { - return carrierName.toString(); - } - } - return null; - } - - @Override - public boolean hasSim() { - return mSubscriptionManager.getActiveSubscriptionInfoCount() > 0; - } - - @Override - public boolean isRoaming() { - return mSubscriptionManager.isNetworkRoaming(mSubId); - } - - @Override - public int[] getMccMnc() { - int mcc = 0; - int mnc = 0; - final SubscriptionInfo subInfo = getActiveSubscriptionInfo(); - if (subInfo != null) { - mcc = subInfo.getMcc(); - mnc = subInfo.getMnc(); - } - return new int[]{mcc, mnc}; - } - - @Override - public String getSimOperatorNumeric() { - // For L_MR1 we return the canonicalized (xxxxxx) string - return getMccMncString(getMccMnc()); - } - - @Override - public String getSelfRawNumber(final boolean allowOverride) { - if (allowOverride) { - final String userDefinedNumber = getNumberFromPrefs(mContext, mSubId); - if (!TextUtils.isEmpty(userDefinedNumber)) { - return userDefinedNumber; - } - } - - final SubscriptionInfo subInfo = getActiveSubscriptionInfo(); - if (subInfo != null) { - String phoneNumber = subInfo.getNumber(); - if (TextUtils.isEmpty(phoneNumber) && LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { - LogUtil.d(TAG, "SubscriptionInfo phone number for self is empty!"); - } - return phoneNumber; - } - LogUtil.w(TAG, "PhoneUtils.getSelfRawNumber: subInfo is null for " + mSubId); - throw new IllegalStateException("No active subscription"); - } - - @Override - public SubscriptionInfo getActiveSubscriptionInfo() { - try { - final SubscriptionInfo subInfo = - mSubscriptionManager.getActiveSubscriptionInfo(mSubId); - if (subInfo == null) { - if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { - // This is possible if the sub id is no longer available. - LogUtil.d(TAG, "PhoneUtils.getActiveSubscriptionInfo(): empty sub info for " - + mSubId); - } - } - return subInfo; - } catch (Exception e) { - LogUtil.e(TAG, "PhoneUtils.getActiveSubscriptionInfo: system exception for " - + mSubId, e); - } - return null; - } - - @Override - public List<SubscriptionInfo> getActiveSubscriptionInfoList() { - final List<SubscriptionInfo> subscriptionInfos = - mSubscriptionManager.getActiveSubscriptionInfoList(); - if (subscriptionInfos != null) { - return subscriptionInfos; - } - return EMPTY_SUBSCRIPTION_LIST; - } - - @Override - public int getEffectiveSubId(int subId) { - if (subId == ParticipantData.DEFAULT_SELF_SUB_ID) { - return getDefaultSmsSubscriptionId(); - } - return subId; - } - - @Override - public void registerOnSubscriptionsChangedListener( - SubscriptionManager.OnSubscriptionsChangedListener listener) { - mSubscriptionManager.addOnSubscriptionsChangedListener(listener); - } - - @Override - public SmsManager getSmsManager() { - return SmsManager.getSmsManagerForSubscriptionId(mSubId); - } - - @Override - public int getDefaultSmsSubscriptionId() { - final int systemDefaultSubId = SmsManager.getDefaultSmsSubscriptionId(); - if (systemDefaultSubId < 0) { - // Always use -1 for any negative subId from system - return ParticipantData.DEFAULT_SELF_SUB_ID; - } - return systemDefaultSubId; - } - - @Override - public boolean getHasPreferredSmsSim() { - return getDefaultSmsSubscriptionId() != ParticipantData.DEFAULT_SELF_SUB_ID; - } - - @Override - public int getActiveSubscriptionCount() { - return mSubscriptionManager.getActiveSubscriptionInfoCount(); - } - - @Override - public int getEffectiveIncomingSubIdFromSystem(Intent intent, String extraName) { - return getEffectiveIncomingSubIdFromSystem(intent.getIntExtra(extraName, - ParticipantData.DEFAULT_SELF_SUB_ID)); - } - - private int getEffectiveIncomingSubIdFromSystem(int subId) { - if (subId < 0) { - if (mSubscriptionManager.getActiveSubscriptionInfoCount() > 1) { - // For multi-SIM device, we can not decide which SIM to use if system - // does not know either. So just make it the invalid sub id. - return ParticipantData.DEFAULT_SELF_SUB_ID; - } - // For single-SIM device, it must come from the only SIM we have - return getDefaultSmsSubscriptionId(); - } - return subId; - } - - @Override - public int getSubIdFromTelephony(Cursor cursor, int subIdIndex) { - return getEffectiveIncomingSubIdFromSystem(cursor.getInt(subIdIndex)); - } - - @Override - public boolean isDataRoamingEnabled() { - final SubscriptionInfo subInfo = getActiveSubscriptionInfo(); - if (subInfo == null) { - // There is nothing we can do if system give us empty sub info - LogUtil.e(TAG, "PhoneUtils.isDataRoamingEnabled: system return empty sub info for " - + mSubId); - return false; - } - return subInfo.getDataRoaming() != SubscriptionManager.DATA_ROAMING_DISABLE; - } - - @Override - public boolean isMobileDataEnabled() { - boolean mobileDataEnabled = false; - try { - final Class cmClass = mTelephonyManager.getClass(); - final Method method = cmClass.getDeclaredMethod("getDataEnabled", Integer.TYPE); - method.setAccessible(true); // Make the method callable - // get the setting for "mobile data" - mobileDataEnabled = (Boolean) method.invoke( - mTelephonyManager, Integer.valueOf(mSubId)); - } catch (final Exception e) { - LogUtil.e(TAG, "PhoneUtil.isMobileDataEnabled: system api not found", e); - } - return mobileDataEnabled; - - } - - @Override - public HashSet<String> getNormalizedSelfNumbers() { - final HashSet<String> numbers = new HashSet<>(); - for (SubscriptionInfo info : getActiveSubscriptionInfoList()) { - numbers.add(PhoneUtils.get(info.getSubscriptionId()).getCanonicalForSelf( - true/*allowOverride*/)); - } - return numbers; - } - } - - /** - * A convenient get() method that uses the default SIM. Use this when SIM is - * not relevant, e.g. isDefaultSmsApp - * - * @return an instance of PhoneUtils for default SIM - */ - public static PhoneUtils getDefault() { - return Factory.get().getPhoneUtils(ParticipantData.DEFAULT_SELF_SUB_ID); - } - - /** - * Get an instance of PhoneUtils associated with a specific SIM, which is also platform - * specific. - * - * @param subId The SIM's subscription ID - * @return the instance - */ - public static PhoneUtils get(int subId) { - return Factory.get().getPhoneUtils(subId); - } - - public LMr1 toLMr1() { - if (OsUtil.isAtLeastL_MR1()) { - return (LMr1) this; - } else { - Assert.fail("PhoneUtils.toLMr1(): invalid OS version"); - return null; - } - } - - /** - * Check if this device supports SMS - * - * @return true if SMS is supported, false otherwise - */ - public boolean isSmsCapable() { - return mTelephonyManager.isSmsCapable(); - } - - /** - * Check if this device supports voice calling - * - * @return true if voice calling is supported, false otherwise - */ - public boolean isVoiceCapable() { - return mTelephonyManager.isVoiceCapable(); - } - - /** - * Get the ISO country code from system locale setting - * - * @return the ISO country code from system locale - */ - private static String getLocaleCountry() { - final String country = Locale.getDefault().getCountry(); - if (TextUtils.isEmpty(country)) { - return null; - } - return country.toUpperCase(); - } - - /** - * Get ISO country code from the SIM, if not available, fall back to locale - * - * @return SIM or locale ISO country code - */ - public String getSimOrDefaultLocaleCountry() { - String country = getSimCountry(); - if (country == null) { - country = getLocaleCountry(); - } - return country; - } - - // Get or set the cache of canonicalized phone numbers for a specific country - private static ArrayMap<String, String> getOrAddCountryMapInCacheLocked(String country) { - if (country == null) { - country = ""; - } - ArrayMap<String, String> countryMap = sCanonicalPhoneNumberCache.get(country); - if (countryMap == null) { - countryMap = new ArrayMap<>(); - sCanonicalPhoneNumberCache.put(country, countryMap); - } - return countryMap; - } - - // Get canonicalized phone number from cache - private static String getCanonicalFromCache(final String phoneText, String country) { - synchronized (sCanonicalPhoneNumberCache) { - final ArrayMap<String, String> countryMap = getOrAddCountryMapInCacheLocked(country); - return countryMap.get(phoneText); - } - } - - // Put canonicalized phone number into cache - private static void putCanonicalToCache(final String phoneText, String country, - final String canonical) { - synchronized (sCanonicalPhoneNumberCache) { - final ArrayMap<String, String> countryMap = getOrAddCountryMapInCacheLocked(country); - countryMap.put(phoneText, canonical); - } - } - - /** - * Utility method to parse user input number into standard E164 number. - * - * @param phoneText Phone number text as input by user. - * @param country ISO country code based on which to parse the number. - * @return E164 phone number. Returns null in case parsing failed. - */ - private static String getValidE164Number(final String phoneText, final String country) { - final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); - try { - final PhoneNumber phoneNumber = phoneNumberUtil.parse(phoneText, country); - if (phoneNumber != null && phoneNumberUtil.isValidNumber(phoneNumber)) { - return phoneNumberUtil.format(phoneNumber, PhoneNumberFormat.E164); - } - } catch (final NumberParseException e) { - LogUtil.e(TAG, "PhoneUtils.getValidE164Number(): Not able to parse phone number " - + LogUtil.sanitizePII(phoneText) + " for country " + country); - } - return null; - } - - /** - * Canonicalize phone number using system locale country - * - * @param phoneText The phone number to canonicalize - * @return the canonicalized number - */ - public String getCanonicalBySystemLocale(final String phoneText) { - return getCanonicalByCountry(phoneText, getLocaleCountry()); - } - - /** - * Canonicalize phone number using SIM's country, may fall back to system locale country - * if SIM country can not be obtained - * - * @param phoneText The phone number to canonicalize - * @return the canonicalized number - */ - public String getCanonicalBySimLocale(final String phoneText) { - return getCanonicalByCountry(phoneText, getSimOrDefaultLocaleCountry()); - } - - /** - * Canonicalize phone number using a country code. - * This uses an internal cache per country to speed up. - * - * @param phoneText The phone number to canonicalize - * @param country The ISO country code to use - * @return the canonicalized number, or the original number if can't be parsed - */ - private String getCanonicalByCountry(final String phoneText, final String country) { - Assert.notNull(phoneText); - - String canonicalNumber = getCanonicalFromCache(phoneText, country); - if (canonicalNumber != null) { - return canonicalNumber; - } - canonicalNumber = getValidE164Number(phoneText, country); - if (canonicalNumber == null) { - // If we can't normalize this number, we just use the display string number. - // This is possible for short codes and other non-localizable numbers. - canonicalNumber = phoneText; - } - putCanonicalToCache(phoneText, country, canonicalNumber); - return canonicalNumber; - } - - /** - * Canonicalize the self (per SIM) phone number - * - * @param allowOverride whether to use the override number in app settings - * @return the canonicalized self phone number - */ - public String getCanonicalForSelf(final boolean allowOverride) { - String selfNumber = null; - try { - selfNumber = getSelfRawNumber(allowOverride); - } catch (IllegalStateException e) { - // continue; - } - if (selfNumber == null) { - return ""; - } - return getCanonicalBySimLocale(selfNumber); - } - - /** - * Get the SIM's phone number in NATIONAL format with only digits, used in sending - * as LINE1NOCOUNTRYCODE macro in mms_config - * - * @return all digits national format number of the SIM - */ - public String getSimNumberNoCountryCode() { - String selfNumber = null; - try { - selfNumber = getSelfRawNumber(false/*allowOverride*/); - } catch (IllegalStateException e) { - // continue - } - if (selfNumber == null) { - selfNumber = ""; - } - final String country = getSimCountry(); - final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); - try { - final PhoneNumber phoneNumber = phoneNumberUtil.parse(selfNumber, country); - if (phoneNumber != null && phoneNumberUtil.isValidNumber(phoneNumber)) { - return phoneNumberUtil - .format(phoneNumber, PhoneNumberFormat.NATIONAL) - .replaceAll("\\D", ""); - } - } catch (final NumberParseException e) { - LogUtil.e(TAG, "PhoneUtils.getSimNumberNoCountryCode(): Not able to parse phone number " - + LogUtil.sanitizePII(selfNumber) + " for country " + country); - } - return selfNumber; - - } - - /** - * Format a phone number for displaying, using system locale country. - * If the country code matches between the system locale and the input phone number, - * it will be formatted into NATIONAL format, otherwise, the INTERNATIONAL format - * - * @param phoneText The original phone text - * @return formatted number - */ - public String formatForDisplay(final String phoneText) { - // Only format a valid number which length >=6 - if (TextUtils.isEmpty(phoneText) || - phoneText.replaceAll("\\D", "").length() < MINIMUM_PHONE_NUMBER_LENGTH_TO_FORMAT) { - return phoneText; - } - final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); - final String systemCountry = getLocaleCountry(); - final int systemCountryCode = phoneNumberUtil.getCountryCodeForRegion(systemCountry); - try { - final PhoneNumber parsedNumber = phoneNumberUtil.parse(phoneText, systemCountry); - final PhoneNumberFormat phoneNumberFormat = - (systemCountryCode > 0 && parsedNumber.getCountryCode() == systemCountryCode) ? - PhoneNumberFormat.NATIONAL : PhoneNumberFormat.INTERNATIONAL; - return phoneNumberUtil.format(parsedNumber, phoneNumberFormat); - } catch (NumberParseException e) { - LogUtil.e(TAG, "PhoneUtils.formatForDisplay: invalid phone number " - + LogUtil.sanitizePII(phoneText) + " with country " + systemCountry); - return phoneText; - } - } - - /** - * Is Messaging the default SMS app? - * - On KLP+ this checks the system setting. - * - On JB (and below) this always returns true, since the setting was added in KLP. - */ - public boolean isDefaultSmsApp() { - if (OsUtil.isAtLeastKLP()) { - final String configuredApplication = Telephony.Sms.getDefaultSmsPackage(mContext); - return mContext.getPackageName().equals(configuredApplication); - } - return true; - } - - /** - * Get default SMS app package name - * - * @return the package name of default SMS app - */ - public String getDefaultSmsApp() { - if (OsUtil.isAtLeastKLP()) { - return Telephony.Sms.getDefaultSmsPackage(mContext); - } - return null; - } - - /** - * Determines if SMS is currently enabled on this device. - * - Device must support SMS - * - On KLP+ we must be set as the default SMS app - */ - public boolean isSmsEnabled() { - return isSmsCapable() && isDefaultSmsApp(); - } - - /** - * Returns the name of the default SMS app, or the empty string if there is - * an error or there is no default app (e.g. JB and below). - */ - public String getDefaultSmsAppLabel() { - if (OsUtil.isAtLeastKLP()) { - final String packageName = Telephony.Sms.getDefaultSmsPackage(mContext); - final PackageManager pm = mContext.getPackageManager(); - try { - final ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0); - return pm.getApplicationLabel(appInfo).toString(); - } catch (NameNotFoundException e) { - // Fall through and return empty string - } - } - return ""; - } - - /** - * Gets the state of Airplane Mode. - * - * @return true if enabled. - */ - @SuppressWarnings("deprecation") - public boolean isAirplaneModeOn() { - if (OsUtil.isAtLeastJB_MR1()) { - return Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.AIRPLANE_MODE_ON, 0) != 0; - } else { - return Settings.System.getInt(mContext.getContentResolver(), - Settings.System.AIRPLANE_MODE_ON, 0) != 0; - } - } - - public static String getMccMncString(int[] mccmnc) { - if (mccmnc == null || mccmnc.length != 2) { - return "000000"; - } - return String.format("%03d%03d", mccmnc[0], mccmnc[1]); - } - - public static String canonicalizeMccMnc(final String mcc, final String mnc) { - try { - return String.format("%03d%03d", Integer.parseInt(mcc), Integer.parseInt(mnc)); - } catch (final NumberFormatException e) { - // Return invalid as is - LogUtil.w(TAG, "canonicalizeMccMnc: invalid mccmnc:" + mcc + " ," + mnc); - } - return mcc + mnc; - } - - /** - * Returns whether the given destination is valid for sending SMS/MMS message. - */ - public static boolean isValidSmsMmsDestination(final String destination) { - return PhoneNumberUtils.isWellFormedSmsAddress(destination) || - MmsSmsUtils.isEmailAddress(destination); - } - - public interface SubscriptionRunnable { - void runForSubscription(int subId); - } - - /** - * A convenience method for iterating through all active subscriptions - * - * @param runnable a {@link SubscriptionRunnable} for performing work on each subscription. - */ - public static void forEachActiveSubscription(final SubscriptionRunnable runnable) { - if (OsUtil.isAtLeastL_MR1()) { - final List<SubscriptionInfo> subscriptionList = - getDefault().toLMr1().getActiveSubscriptionInfoList(); - for (final SubscriptionInfo subscriptionInfo : subscriptionList) { - runnable.runForSubscription(subscriptionInfo.getSubscriptionId()); - } - } else { - runnable.runForSubscription(ParticipantData.DEFAULT_SELF_SUB_ID); - } - } - - private static String getNumberFromPrefs(final Context context, final int subId) { - final BuglePrefs prefs = BuglePrefs.getSubscriptionPrefs(subId); - final String mmsPhoneNumberPrefKey = - context.getString(R.string.mms_phone_number_pref_key); - final String userDefinedNumber = prefs.getString(mmsPhoneNumberPrefKey, null); - if (!TextUtils.isEmpty(userDefinedNumber)) { - return userDefinedNumber; - } - return null; - } -} diff --git a/src/com/android/messaging/util/RingtoneUtil.java b/src/com/android/messaging/util/RingtoneUtil.java deleted file mode 100644 index a7facfb..0000000 --- a/src/com/android/messaging/util/RingtoneUtil.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; -import android.net.Uri; -import android.provider.Settings; -import android.text.TextUtils; - -import com.android.messaging.Factory; -import com.android.messaging.R; - -public class RingtoneUtil { - /** - * Return a ringtone Uri for the string representation passed in. Use the app - * and system defaults as fallbacks - * @param ringtoneString is the ringtone to resolve - * @return the Uri of the ringtone or the fallback ringtone - */ - public static Uri getNotificationRingtoneUri(String ringtoneString) { - if (ringtoneString == null) { - // No override specified, fall back to system-wide setting. - final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); - final Context context = Factory.get().getApplicationContext(); - final String prefKey = context.getString(R.string.notification_sound_pref_key); - ringtoneString = prefs.getString(prefKey, null); - } - - if (!TextUtils.isEmpty(ringtoneString)) { - // We have set a value, even if it is the default Uri at some point - return Uri.parse(ringtoneString); - } else if (ringtoneString == null) { - // We have no setting specified (== null), so we default to the system default - return Settings.System.DEFAULT_NOTIFICATION_URI; - } else { - // An empty string (== "") here is the result of selecting "None" as the ringtone - return null; - } - } -} diff --git a/src/com/android/messaging/util/SafeAsyncTask.java b/src/com/android/messaging/util/SafeAsyncTask.java deleted file mode 100644 index 1cce6e9..0000000 --- a/src/com/android/messaging/util/SafeAsyncTask.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Debug; -import android.os.SystemClock; - -import com.android.messaging.Factory; -import com.android.messaging.util.Assert.RunsOnAnyThread; - -/** - * Wrapper class which provides explicit API for: - * <ol> - * <li>Threading policy choice - Users of this class should use the explicit API instead of - * {@link #execute} which uses different threading policy on different OS versions. - * <li>Enforce creation on main thread as required by AsyncTask - * <li>Enforce that the background task does not take longer than expected. - * </ol> - */ -public abstract class SafeAsyncTask<Params, Progress, Result> - extends AsyncTask<Params, Progress, Result> { - private static final long DEFAULT_MAX_EXECUTION_TIME_MILLIS = 10 * 1000; // 10 seconds - - /** This is strongly discouraged as it can block other AsyncTasks indefinitely. */ - public static final long UNBOUNDED_TIME = Long.MAX_VALUE; - - private static final String WAKELOCK_ID = "bugle_safe_async_task_wakelock"; - protected static final int WAKELOCK_OP = 1000; - private static WakeLockHelper sWakeLock = new WakeLockHelper(WAKELOCK_ID); - - private final long mMaxExecutionTimeMillis; - private final boolean mCancelExecutionOnTimeout; - private boolean mThreadPoolRequested; - - public SafeAsyncTask() { - this(DEFAULT_MAX_EXECUTION_TIME_MILLIS, false); - } - - public SafeAsyncTask(final long maxTimeMillis) { - this(maxTimeMillis, false); - } - - /** - * @param maxTimeMillis maximum expected time for the background operation. This is just - * a diagnostic tool to catch unexpectedly long operations. If an operation does take - * longer than expected, it is fine to increase this argument. If the value is larger - * than a minute, you should consider using a dedicated thread so as not to interfere - * with other AsyncTasks. - * - * <p>Use {@link #UNBOUNDED_TIME} if you do not know the maximum expected time. This - * is strongly discouraged as it can block other AsyncTasks indefinitely. - * - * @param cancelExecutionOnTimeout whether to attempt to cancel the task execution on timeout. - * If this is set, at execution timeout we will call cancel(), so doInBackgroundTimed() - * should periodically check if the task is to be cancelled and finish promptly if - * possible, and handle the cancel event in onCancelled(). Also, at the end of execution - * we will not crash the execution if it went over limit since we explicitly canceled it. - */ - public SafeAsyncTask(final long maxTimeMillis, final boolean cancelExecutionOnTimeout) { - Assert.isMainThread(); // AsyncTask has to be created on the main thread - mMaxExecutionTimeMillis = maxTimeMillis; - mCancelExecutionOnTimeout = cancelExecutionOnTimeout; - } - - public final SafeAsyncTask<Params, Progress, Result> executeOnThreadPool( - final Params... params) { - Assert.isMainThread(); // AsyncTask requires this - mThreadPoolRequested = true; - executeOnExecutor(THREAD_POOL_EXECUTOR, params); - return this; - } - - protected abstract Result doInBackgroundTimed(final Params... params); - - @Override - protected final Result doInBackground(final Params... params) { - // This enforces that executeOnThreadPool was called, not execute. Ideally, we would - // make execute throw an exception, but since it is final, we cannot override it. - Assert.isTrue(mThreadPoolRequested); - - if (mCancelExecutionOnTimeout) { - ThreadUtil.getMainThreadHandler().postDelayed(new Runnable() { - @Override - public void run() { - if (getStatus() == Status.RUNNING) { - // Cancel the task if it's still running. - LogUtil.w(LogUtil.BUGLE_TAG, String.format("%s timed out and is canceled", - this)); - cancel(true /* mayInterruptIfRunning */); - } - } - }, mMaxExecutionTimeMillis); - } - - final long startTime = SystemClock.elapsedRealtime(); - try { - return doInBackgroundTimed(params); - } finally { - final long executionTime = SystemClock.elapsedRealtime() - startTime; - if (executionTime > mMaxExecutionTimeMillis) { - LogUtil.w(LogUtil.BUGLE_TAG, String.format("%s took %dms", this, executionTime)); - // Don't crash if debugger is attached or if we are asked to cancel on timeout. - if (!Debug.isDebuggerConnected() && !mCancelExecutionOnTimeout) { - Assert.fail(this + " took too long"); - } - } - } - - } - - @Override - protected void onPostExecute(final Result result) { - // No need to use AsyncTask at all if there is no onPostExecute - Assert.fail("Use SafeAsyncTask.executeOnThreadPool"); - } - - /** - * This provides a way for people to run async tasks but without onPostExecute. - * This can be called on any thread. - * - * Run code in a thread using AsyncTask's thread pool. - * - * To enable wakelock during the execution, see {@link #executeOnThreadPool(Runnable, boolean)} - * - * @param runnable The Runnable to execute asynchronously - */ - @RunsOnAnyThread - public static void executeOnThreadPool(final Runnable runnable) { - executeOnThreadPool(runnable, false); - } - - /** - * This provides a way for people to run async tasks but without onPostExecute. - * This can be called on any thread. - * - * Run code in a thread using AsyncTask's thread pool. - * - * @param runnable The Runnable to execute asynchronously - * @param withWakeLock when set, a wake lock will be held for the duration of the runnable - * execution - */ - public static void executeOnThreadPool(final Runnable runnable, final boolean withWakeLock) { - if (withWakeLock) { - final Intent intent = new Intent(); - sWakeLock.acquire(Factory.get().getApplicationContext(), intent, WAKELOCK_OP); - THREAD_POOL_EXECUTOR.execute(new Runnable() { - @Override - public void run() { - try { - runnable.run(); - } finally { - sWakeLock.release(intent, WAKELOCK_OP); - } - } - }); - } else { - THREAD_POOL_EXECUTOR.execute(runnable); - } - } -} diff --git a/src/com/android/messaging/util/SwitchCompatUtils.java b/src/com/android/messaging/util/SwitchCompatUtils.java deleted file mode 100644 index b5d1ed5..0000000 --- a/src/com/android/messaging/util/SwitchCompatUtils.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.support.v7.graphics.drawable.DrawableWrapper; -import android.support.v7.widget.SwitchCompat; -import android.util.TypedValue; - -/* Most methods in this file are copied from - * v7/appcompat/src/android/support/v7/internal/widget/TintManager.java. It would be better if - * we could have just extended the TintManager but this is a final class that we do not have - * access to. */ - -/** - * Util methods for the SwitchCompat widget - */ -public class SwitchCompatUtils { - /** - * Given a color and a SwitchCompat view, updates the SwitchCompat to appear with the appropiate - * color when enabled and checked - */ - public static void updateSwitchCompatColor(SwitchCompat switchCompat, final int color) { - final Context context = switchCompat.getContext(); - final TypedValue typedValue = new TypedValue(); - - switchCompat.setThumbDrawable(getColorTintedDrawable(switchCompat.getThumbDrawable(), - getSwitchThumbColorStateList(context, color, typedValue), - PorterDuff.Mode.MULTIPLY)); - - switchCompat.setTrackDrawable(getColorTintedDrawable(switchCompat.getTrackDrawable(), - getSwitchTrackColorStateList(context, color, typedValue), PorterDuff.Mode.SRC_IN)); - } - - private static Drawable getColorTintedDrawable(Drawable oldDrawable, - final ColorStateList colorStateList, final PorterDuff.Mode mode) { - final int[] thumbState = oldDrawable.isStateful() ? oldDrawable.getState() : null; - if (oldDrawable instanceof DrawableWrapper) { - oldDrawable = ((DrawableWrapper) oldDrawable).getWrappedDrawable(); - } - final Drawable newDrawable = new TintDrawableWrapper(oldDrawable, colorStateList, mode); - if (thumbState != null) { - newDrawable.setState(thumbState); - } - return newDrawable; - } - - private static ColorStateList getSwitchThumbColorStateList(final Context context, - final int color, final TypedValue typedValue) { - final int[][] states = new int[3][]; - final int[] colors = new int[3]; - int i = 0; - // Disabled state - states[i] = new int[] { -android.R.attr.state_enabled }; - colors[i] = getColor(Color.parseColor("#ffbdbdbd"), 1f); - i++; - states[i] = new int[] { android.R.attr.state_checked }; - colors[i] = color; - i++; - // Default enabled state - states[i] = new int[0]; - colors[i] = getThemeAttrColor(context, typedValue, - android.support.v7.appcompat.R.attr.colorSwitchThumbNormal); - i++; - return new ColorStateList(states, colors); - } - - private static ColorStateList getSwitchTrackColorStateList(final Context context, - final int color, final TypedValue typedValue) { - final int[][] states = new int[3][]; - final int[] colors = new int[3]; - int i = 0; - // Disabled state - states[i] = new int[] { -android.R.attr.state_enabled }; - colors[i] = getThemeAttrColor(context, typedValue, android.R.attr.colorForeground, 0.1f); - i++; - states[i] = new int[] { android.R.attr.state_checked }; - colors[i] = getColor(color, 0.3f); - i++; - // Default enabled state - states[i] = new int[0]; - colors[i] = getThemeAttrColor(context, typedValue, android.R.attr.colorForeground, 0.3f); - i++; - return new ColorStateList(states, colors); - } - - private static int getThemeAttrColor(final Context context, final TypedValue typedValue, - final int attr) { - if (context.getTheme().resolveAttribute(attr, typedValue, true)) { - if (typedValue.type >= TypedValue.TYPE_FIRST_INT - && typedValue.type <= TypedValue.TYPE_LAST_INT) { - return typedValue.data; - } else if (typedValue.type == TypedValue.TYPE_STRING) { - return context.getResources().getColor(typedValue.resourceId); - } - } - return 0; - } - - private static int getThemeAttrColor(final Context context, final TypedValue typedValue, - final int attr, final float alpha) { - final int color = getThemeAttrColor(context, typedValue, attr); - return getColor(color, alpha); - } - - private static int getColor(int color, float alpha) { - final int originalAlpha = Color.alpha(color); - // Return the color, multiplying the original alpha by the disabled value - return (color & 0x00ffffff) | (Math.round(originalAlpha * alpha) << 24); - } -} diff --git a/src/com/android/messaging/util/TextUtil.java b/src/com/android/messaging/util/TextUtil.java deleted file mode 100644 index b240396..0000000 --- a/src/com/android/messaging/util/TextUtil.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.support.annotation.Nullable; - -public class TextUtil { - /** - * Returns true if the string is empty, null or only whitespace. - */ - public static boolean isAllWhitespace(@Nullable String string) { - if (string == null || string.isEmpty()) { - return true; - } - - for (int i = 0; i < string.length(); ++i) { - if (!Character.isWhitespace(string.charAt(i))) { - return false; - } - } - - return true; - } - - /** - * Taken from PhoneNumberUtils, where it is only available in API 21+ Replaces all unicode - * (e.g. Arabic, Persian) digits with their decimal digit equivalents. - * - * @param number the number to perform the replacement on. - * @return the replaced number. - */ - public static String replaceUnicodeDigits(String number) { - StringBuilder normalizedDigits = new StringBuilder(number.length()); - for (char c : number.toCharArray()) { - int digit = Character.digit(c, 10); - if (digit != -1) { - normalizedDigits.append(digit); - } else { - normalizedDigits.append(c); - } - } - return normalizedDigits.toString(); - } - - /** - * Appends text to the stringBuilder. - * If stringBuilder already has content, separator is prepended to create a separator between - * entries. - * @param stringBuilder The stringBuilder to add to - * @param text The text to append - * @param separator The separator to add if there is already text, typically "," or "\n" - */ - public static void appendWithSeparator(final StringBuilder stringBuilder, final String text, - final String separator) { - if (stringBuilder.length() > 0) { - stringBuilder.append(separator); - } - stringBuilder.append(text); - } -} diff --git a/src/com/android/messaging/util/ThreadUtil.java b/src/com/android/messaging/util/ThreadUtil.java deleted file mode 100644 index 3b935d8..0000000 --- a/src/com/android/messaging/util/ThreadUtil.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.os.Handler; -import android.os.Looper; - -public class ThreadUtil { - private static final Handler sHandler = new Handler(Looper.getMainLooper()); - - public static Handler getMainThreadHandler() { - return sHandler; - } -} diff --git a/src/com/android/messaging/util/TintDrawableWrapper.java b/src/com/android/messaging/util/TintDrawableWrapper.java deleted file mode 100644 index e6ea4bd..0000000 --- a/src/com/android/messaging/util/TintDrawableWrapper.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.res.ColorStateList; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.support.v7.graphics.drawable.DrawableWrapper; - -/* - * This is directly copied from v7/appcompat/src/android/support/v7/internal/widget/TintManager.java - */ - -/** - * A {@link DrawableWrapper} which updates it's color filter using a {@link ColorStateList}. - */ -class TintDrawableWrapper extends DrawableWrapper { - private final ColorStateList mTintStateList; - private final PorterDuff.Mode mTintMode; - private int mCurrentColor; - public TintDrawableWrapper(Drawable drawable, ColorStateList tintStateList) { - this(drawable, tintStateList, PorterDuff.Mode.SRC_IN); - } - public TintDrawableWrapper(Drawable drawable, ColorStateList tintStateList, - PorterDuff.Mode tintMode) { - super(drawable); - mTintStateList = tintStateList; - mTintMode = tintMode; - } - @Override - public boolean isStateful() { - return (mTintStateList != null && mTintStateList.isStateful()) || super.isStateful(); - } - @Override - public boolean setState(int[] stateSet) { - boolean handled = super.setState(stateSet); - handled = updateTint(stateSet) || handled; - return handled; - } - private boolean updateTint(int[] state) { - if (mTintStateList != null) { - final int color = mTintStateList.getColorForState(state, mCurrentColor); - if (color != mCurrentColor) { - if (color != Color.TRANSPARENT) { - setColorFilter(color, mTintMode); - } else { - clearColorFilter(); - } - mCurrentColor = color; - return true; - } - } - return false; - } -} diff --git a/src/com/android/messaging/util/Trace.java b/src/com/android/messaging/util/Trace.java deleted file mode 100644 index da1e87c..0000000 --- a/src/com/android/messaging/util/Trace.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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.android.messaging.util; - - -import android.annotation.TargetApi; -import android.os.Build; - -/** - * Helper class for systrace (see http://developer.android.com/tools/help/systrace.html).<p> - * To enable, set log.tag.Bugle_Trace (defined by {@link #TAG} to VERBOSE before - * the process starts.<p> - * Note that this will run only on JBMR2 or later; on earlier platforms or if the log - * tag isn't set, calls to {@link #beginSection(String)} or {@link #endSection()} are no-ops. <p> - * Internally, calls dispatch to either a class that actually does work or a class that doesn't. - * This avoids Dalvik complaining when it loads the class on earlier platforms that the - * opcodes aren't available, and, according to the Dalvik team, using vtable dispatching for - * something like this should be faster than if (OsUtil.isAtLeast...()) on each call. - */ -public final class Trace { - private static final String TAG = "Bugle_Trace"; - private abstract static class AbstractTrace { - abstract void beginSection(String sectionName); - abstract void endSection(); - } - - private static final AbstractTrace sTrace; - - // Static initializer to pick the correct trace class to handle tracing. - static { - // Use android.util.Log instead of LogUtil here to avoid pulling in Gservices - // too early in app startup. - if (OsUtil.isAtLeastJB_MR2() && - android.util.Log.isLoggable(TAG, android.util.Log.VERBOSE)) { - sTrace = new TraceJBMR2(); - } else { - sTrace = new TraceShim(); - } - } - - /** - * Writes a trace message to indicate that a given section of code has begun. This call must - * be followed by a corresponding call to {@link #endSection()} on the same thread. - * - * <p class="note"> At this time the vertical bar character '|', newline character '\n', and - * null character '\0' are used internally by the tracing mechanism. If sectionName contains - * these characters they will be replaced with a space character in the trace. - * - * @param sectionName The name of the code section to appear in the trace. This may be at - * most 127 Unicode code units long. - */ - public static void beginSection(String sectionName) { - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - LogUtil.v(TAG, "beginSection() " + sectionName); - } - sTrace.beginSection(sectionName); - } - - /** - * Writes a trace message to indicate that a given section of code has ended. This call must - * be preceeded by a corresponding call to {@link #beginSection(String)}. Calling this method - * will mark the end of the most recently begun section of code, so care must be taken to - * ensure that beginSection / endSection pairs are properly nested and called from the same - * thread. - */ - public static void endSection() { - sTrace.endSection(); - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - LogUtil.v(TAG, "endSection()"); - } - } - - /** - * Internal class that we use if we really did enable tracing. - */ - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) - private static final class TraceJBMR2 extends AbstractTrace { - @Override - void beginSection(String sectionName) { - android.os.Trace.beginSection(sectionName); - } - - @Override - void endSection() { - android.os.Trace.endSection(); - } - } - - /** - * Dummy class that we use if we aren't really tracing. - */ - private static final class TraceShim extends AbstractTrace { - @Override - void beginSection(String sectionName) { - } - - @Override - void endSection() { - } - } -} diff --git a/src/com/android/messaging/util/Typefaces.java b/src/com/android/messaging/util/Typefaces.java deleted file mode 100644 index eb8562c..0000000 --- a/src/com/android/messaging/util/Typefaces.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.graphics.Typeface; - -/** - * Provides access to typefaces used by code. Specially important for typefaces coming from assets, - * which appear (from platform code inspection) to not be cached. - * Note: Considered making this a singleton provided by factory/appcontext, but seemed too simple, - * not worth stubbing. - */ -public class Typefaces { - private static Typeface sRobotoBold; - private static Typeface sRobotoNormal; - - public static Typeface getRobotoBold() { - Assert.isMainThread(); - if (sRobotoBold == null) { - sRobotoBold = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD); - } - return sRobotoBold; - } - - public static Typeface getRobotoNormal() { - Assert.isMainThread(); - if (sRobotoNormal == null) { - sRobotoNormal = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL); - } - return sRobotoNormal; - } -} diff --git a/src/com/android/messaging/util/UiUtils.java b/src/com/android/messaging/util/UiUtils.java deleted file mode 100644 index 84fe353..0000000 --- a/src/com/android/messaging/util/UiUtils.java +++ /dev/null @@ -1,438 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.app.Activity; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.pm.ActivityInfo; -import android.content.res.Configuration; -import android.graphics.Color; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.app.ActionBar; -import android.support.v7.app.ActionBarActivity; -import android.text.Html; -import android.text.Spanned; -import android.text.TextPaint; -import android.text.TextUtils; -import android.text.style.URLSpan; -import android.view.Gravity; -import android.view.Surface; -import android.view.View; -import android.view.View.OnLayoutChangeListener; -import android.view.animation.Animation; -import android.view.animation.Animation.AnimationListener; -import android.view.animation.Interpolator; -import android.view.animation.ScaleAnimation; -import android.widget.RemoteViews; -import android.widget.Toast; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.ui.SnackBar; -import com.android.messaging.ui.SnackBar.Placement; -import com.android.messaging.ui.conversationlist.ConversationListActivity; -import com.android.messaging.ui.SnackBarInteraction; -import com.android.messaging.ui.SnackBarManager; -import com.android.messaging.ui.UIIntents; - -import java.lang.reflect.Field; -import java.util.List; - -public class UiUtils { - /** MediaPicker transition duration in ms */ - public static final int MEDIAPICKER_TRANSITION_DURATION = - getApplicationContext().getResources().getInteger( - R.integer.mediapicker_transition_duration); - /** Short transition duration in ms */ - public static final int ASYNCIMAGE_TRANSITION_DURATION = - getApplicationContext().getResources().getInteger( - R.integer.asyncimage_transition_duration); - /** Compose transition duration in ms */ - public static final int COMPOSE_TRANSITION_DURATION = - getApplicationContext().getResources().getInteger( - R.integer.compose_transition_duration); - /** Generic duration for revealing/hiding a view */ - public static final int REVEAL_ANIMATION_DURATION = - getApplicationContext().getResources().getInteger( - R.integer.reveal_view_animation_duration); - - public static final Interpolator DEFAULT_INTERPOLATOR = new CubicBezierInterpolator( - 0.4f, 0.0f, 0.2f, 1.0f); - - public static final Interpolator EASE_IN_INTERPOLATOR = new CubicBezierInterpolator( - 0.4f, 0.0f, 0.8f, 0.5f); - - public static final Interpolator EASE_OUT_INTERPOLATOR = new CubicBezierInterpolator( - 0.0f, 0.0f, 0.2f, 1f); - - /** Show a simple toast at the bottom */ - public static void showToastAtBottom(final int messageId) { - UiUtils.showToastAtBottom(getApplicationContext().getString(messageId)); - } - - /** Show a simple toast at the bottom */ - public static void showToastAtBottom(final String message) { - final Toast toast = Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG); - toast.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0); - toast.show(); - } - - /** Show a simple toast at the default position */ - public static void showToast(final int messageId) { - final Toast toast = Toast.makeText(getApplicationContext(), - getApplicationContext().getString(messageId), Toast.LENGTH_LONG); - toast.setGravity(Gravity.CENTER_HORIZONTAL, 0, 0); - toast.show(); - } - - /** Show a simple toast at the default position */ - public static void showToast(final int pluralsMessageId, final int count) { - final Toast toast = Toast.makeText(getApplicationContext(), - getApplicationContext().getResources().getQuantityString(pluralsMessageId, count), - Toast.LENGTH_LONG); - toast.setGravity(Gravity.CENTER_HORIZONTAL, 0, 0); - toast.show(); - } - - public static void showSnackBar(final Context context, @NonNull final View parentView, - final String message, @Nullable final Runnable runnable, final int runnableLabel, - @Nullable final List<SnackBarInteraction> interactions) { - Assert.notNull(context); - SnackBar.Action action = null; - switch (runnableLabel) { - case SnackBar.Action.SNACK_BAR_UNDO: - action = SnackBar.Action.createUndoAction(runnable); - break; - case SnackBar.Action.SNACK_BAR_RETRY: - action = SnackBar.Action.createRetryAction(runnable); - break; - default : - break; - } - - showSnackBarWithCustomAction(context, parentView, message, action, interactions, - null /* placement */); - } - - public static void showSnackBarWithCustomAction(final Context context, - @NonNull final View parentView, - @NonNull final String message, - @NonNull final SnackBar.Action action, - @Nullable final List<SnackBarInteraction> interactions, - @Nullable final Placement placement) { - Assert.notNull(context); - Assert.isTrue(!TextUtils.isEmpty(message)); - Assert.notNull(action); - SnackBarManager.get() - .newBuilder(parentView) - .setText(message) - .setAction(action) - .withInteractions(interactions) - .withPlacement(placement) - .show(); - } - - /** - * Run the given runnable once after the next layout pass of the view. - */ - public static void doOnceAfterLayoutChange(final View view, final Runnable runnable) { - final OnLayoutChangeListener listener = new OnLayoutChangeListener() { - @Override - public void onLayoutChange(final View v, final int left, final int top, final int right, - final int bottom, final int oldLeft, final int oldTop, final int oldRight, - final int oldBottom) { - // Call the runnable outside the layout pass because very few actions are allowed in - // the layout pass - ThreadUtil.getMainThreadHandler().post(runnable); - view.removeOnLayoutChangeListener(this); - } - }; - view.addOnLayoutChangeListener(listener); - } - - public static boolean isLandscapeMode() { - return Factory.get().getApplicationContext().getResources().getConfiguration().orientation - == Configuration.ORIENTATION_LANDSCAPE; - } - - private static Context getApplicationContext() { - return Factory.get().getApplicationContext(); - } - - public static CharSequence commaEllipsize( - final String text, - final TextPaint paint, - final int width, - final String oneMore, - final String more) { - CharSequence ellipsized = TextUtils.commaEllipsize( - text, - paint, - width, - oneMore, - more); - if (TextUtils.isEmpty(ellipsized)) { - ellipsized = text; - } - return ellipsized; - } - - /** - * Reveals/Hides a view with a scale animation from view center. - * @param view the view to animate - * @param desiredVisibility desired visibility (e.g. View.GONE) for the animated view. - * @param onFinishRunnable an optional runnable called at the end of the animation - */ - public static void revealOrHideViewWithAnimation(final View view, final int desiredVisibility, - @Nullable final Runnable onFinishRunnable) { - final boolean needAnimation = view.getVisibility() != desiredVisibility; - if (needAnimation) { - final float fromScale = desiredVisibility == View.VISIBLE ? 0F : 1F; - final float toScale = desiredVisibility == View.VISIBLE ? 1F : 0F; - final ScaleAnimation showHideAnimation = - new ScaleAnimation(fromScale, toScale, fromScale, toScale, - ScaleAnimation.RELATIVE_TO_SELF, 0.5f, - ScaleAnimation.RELATIVE_TO_SELF, 0.5f); - showHideAnimation.setDuration(REVEAL_ANIMATION_DURATION); - showHideAnimation.setInterpolator(DEFAULT_INTERPOLATOR); - showHideAnimation.setAnimationListener(new AnimationListener() { - @Override - public void onAnimationStart(final Animation animation) { - } - - @Override - public void onAnimationRepeat(final Animation animation) { - } - - @Override - public void onAnimationEnd(final Animation animation) { - if (onFinishRunnable != null) { - // Rather than running this immediately, we post it to happen next so that - // the animation will be completed so that the view can be detached from - // it's window. Otherwise, we may leak memory. - ThreadUtil.getMainThreadHandler().post(onFinishRunnable); - } - } - }); - view.clearAnimation(); - view.startAnimation(showHideAnimation); - // We are playing a view Animation; unlike view property animations, we can commit the - // visibility immediately instead of waiting for animation end. - view.setVisibility(desiredVisibility); - } else if (onFinishRunnable != null) { - // Make sure onFinishRunnable is always executed. - ThreadUtil.getMainThreadHandler().post(onFinishRunnable); - } - } - - public static Rect getMeasuredBoundsOnScreen(final View view) { - final int[] location = new int[2]; - view.getLocationOnScreen(location); - return new Rect(location[0], location[1], - location[0] + view.getMeasuredWidth(), location[1] + view.getMeasuredHeight()); - } - - public static void setStatusBarColor(final Activity activity, final int color) { - if (OsUtil.isAtLeastL()) { - // To achieve the appearance of an 80% opacity blend against a black background, - // each color channel is reduced in value by 20%. - final int blendedRed = (int) Math.floor(0.8 * Color.red(color)); - final int blendedGreen = (int) Math.floor(0.8 * Color.green(color)); - final int blendedBlue = (int) Math.floor(0.8 * Color.blue(color)); - - activity.getWindow().setStatusBarColor( - Color.rgb(blendedRed, blendedGreen, blendedBlue)); - } - } - - public static void lockOrientation(final Activity activity) { - final int orientation = activity.getResources().getConfiguration().orientation; - final int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); - - // rotation tracks the rotation of the device from its natural orientation - // orientation tracks whether the screen is landscape or portrait. - // It is possible to have a rotation of 0 (device in its natural orientation) in portrait - // (phone), or in landscape (tablet), so we have to check both values to determine what to - // pass to setRequestedOrientation. - if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { - if (orientation == Configuration.ORIENTATION_PORTRAIT) { - activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); - } - } else if (rotation == Surface.ROTATION_180 || rotation == Surface.ROTATION_270) { - if (orientation == Configuration.ORIENTATION_PORTRAIT) { - activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT); - } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE); - } - } - } - - public static void unlockOrientation(final Activity activity) { - activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR); - } - - public static int getPaddingStart(final View view) { - return OsUtil.isAtLeastJB_MR1() ? view.getPaddingStart() : view.getPaddingLeft(); - } - - public static int getPaddingEnd(final View view) { - return OsUtil.isAtLeastJB_MR1() ? view.getPaddingEnd() : view.getPaddingRight(); - } - - public static boolean isRtlMode() { - return OsUtil.isAtLeastJB_MR2() && Factory.get().getApplicationContext().getResources() - .getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - } - - /** - * Check if the activity needs to be redirected to permission check - * @return true if {@link Activity#finish()} was called because redirection was performed - */ - public static boolean redirectToPermissionCheckIfNeeded(final Activity activity) { - if (!OsUtil.hasRequiredPermissions()) { - UIIntents.get().launchPermissionCheckActivity(activity); - } else { - // No redirect performed - return false; - } - - // Redirect performed - activity.finish(); - return true; - } - - /** - * Called to check if all conditions are nominal and a "go" for some action, such as deleting - * a message, that requires this app to be the default app. This is also a precondition - * required for sending a draft. - * @return true if all conditions are nominal and we're ready to send a message - */ - public static boolean isReadyForAction() { - final PhoneUtils phoneUtils = PhoneUtils.getDefault(); - - // Have all the conditions been met: - // Supports SMS? - // Has a preferred sim? - // Is the default sms app? - return phoneUtils.isSmsCapable() && - phoneUtils.getHasPreferredSmsSim() && - phoneUtils.isDefaultSmsApp(); - } - - /* - * Removes all html markup from the text and replaces links with the the text and a text version - * of the href. - * @param htmlText HTML markup text - * @return Sanitized string with link hrefs inlined - */ - public static String stripHtml(final String htmlText) { - final StringBuilder result = new StringBuilder(); - final Spanned markup = Html.fromHtml(htmlText); - final String strippedText = markup.toString(); - - final URLSpan[] links = markup.getSpans(0, markup.length() - 1, URLSpan.class); - int currentIndex = 0; - for (final URLSpan link : links) { - final int spanStart = markup.getSpanStart(link); - final int spanEnd = markup.getSpanEnd(link); - if (spanStart > currentIndex) { - result.append(strippedText, currentIndex, spanStart); - } - final String displayText = strippedText.substring(spanStart, spanEnd); - final String linkText = link.getURL(); - result.append(getApplicationContext().getString(R.string.link_display_format, - displayText, linkText)); - currentIndex = spanEnd; - } - if (strippedText.length() > currentIndex) { - result.append(strippedText, currentIndex, strippedText.length()); - } - return result.toString(); - } - - public static void setActionBarShadowVisibility(final ActionBarActivity activity, final boolean visible) { - final ActionBar actionBar = activity.getSupportActionBar(); - actionBar.setElevation(visible ? - activity.getResources().getDimensionPixelSize(R.dimen.action_bar_elevation) : - 0); - final View actionBarView = activity.getWindow().getDecorView().findViewById( - android.support.v7.appcompat.R.id.decor_content_parent); - if (actionBarView != null) { - // AppCompatActionBar has one drawable Field, which is the shadow for the action bar - // set the alpha on that drawable manually - final Field[] fields = actionBarView.getClass().getDeclaredFields(); - try { - for (final Field field : fields) { - if (field.getType().equals(Drawable.class)) { - field.setAccessible(true); - final Drawable shadowDrawable = (Drawable) field.get(actionBarView); - if (shadowDrawable != null) { - shadowDrawable.setAlpha(visible ? 255 : 0); - actionBarView.invalidate(); - return; - } - } - } - } catch (final IllegalAccessException ex) { - // Not expected, we should avoid this via field.setAccessible(true) above - LogUtil.e(LogUtil.BUGLE_TAG, "Error setting shadow visibility", ex); - } - } - } - - /** - * Get the activity that's hosting the view, typically casting view.getContext() as an Activity - * is sufficient, but sometimes the context is a context wrapper, in which case we need to case - * the base context - */ - public static Activity getActivity(final View view) { - if (view == null) { - return null; - } - return getActivity(view.getContext()); - } - - /** - * Get the activity for the supplied context, typically casting context as an Activity - * is sufficient, but sometimes the context is a context wrapper, in which case we need to case - * the base context - */ - public static Activity getActivity(final Context context) { - if (context == null) { - return null; - } - if (context instanceof Activity) { - return (Activity) context; - } - if (context instanceof ContextWrapper) { - return getActivity(((ContextWrapper) context).getBaseContext()); - } - - // We've hit a non-activity context such as an app-context - return null; - } - - public static RemoteViews getWidgetMissingPermissionView(final Context context) { - return new RemoteViews(context.getPackageName(), R.layout.widget_missing_permission); - } -} diff --git a/src/com/android/messaging/util/UriUtil.java b/src/com/android/messaging/util/UriUtil.java deleted file mode 100644 index 4bbc80d..0000000 --- a/src/com/android/messaging/util/UriUtil.java +++ /dev/null @@ -1,393 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.res.AssetFileDescriptor; -import android.media.MediaMetadataRetriever; -import android.net.Uri; -import android.os.ParcelFileDescriptor; -import android.provider.MediaStore; -import android.support.annotation.NonNull; -import android.text.TextUtils; - -import com.android.messaging.Factory; -import com.android.messaging.datamodel.MediaScratchFileProvider; -import com.android.messaging.util.Assert.DoesNotRunOnMainThread; -import com.google.common.io.ByteStreams; -import com.google.common.io.Files; -import com.google.common.io.Resources; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; -import java.net.URLConnection; -import java.util.Arrays; -import java.util.HashSet; - -public class UriUtil { - private static final String SCHEME_SMS = "sms"; - private static final String SCHEME_SMSTO = "smsto"; - private static final String SCHEME_MMS = "mms"; - private static final String SCHEME_MMSTO = "smsto"; - public static final HashSet<String> SMS_MMS_SCHEMES = new HashSet<String>( - Arrays.asList(SCHEME_SMS, SCHEME_MMS, SCHEME_SMSTO, SCHEME_MMSTO)); - - public static final String SCHEME_BUGLE = "bugle"; - public static final HashSet<String> SUPPORTED_SCHEME = new HashSet<String>( - Arrays.asList(ContentResolver.SCHEME_ANDROID_RESOURCE, - ContentResolver.SCHEME_CONTENT, - ContentResolver.SCHEME_FILE, - SCHEME_BUGLE)); - - public static final String SCHEME_TEL = "tel:"; - - /** - * Get a Uri representation of the file path of a resource file. - */ - public static Uri getUriForResourceFile(final String path) { - return TextUtils.isEmpty(path) ? null : Uri.fromFile(new File(path)); - } - - /** - * Extract the path from a file:// Uri, or null if the uri is of other scheme. - */ - public static String getFilePathFromUri(final Uri uri) { - if (!isFileUri(uri)) { - return null; - } - return uri.getPath(); - } - - /** - * Returns whether the given Uri is local or remote. - */ - public static boolean isLocalResourceUri(final Uri uri) { - final String scheme = uri.getScheme(); - return TextUtils.equals(scheme, ContentResolver.SCHEME_ANDROID_RESOURCE) || - TextUtils.equals(scheme, ContentResolver.SCHEME_CONTENT) || - TextUtils.equals(scheme, ContentResolver.SCHEME_FILE); - } - - /** - * Returns whether the given Uri is part of Bugle's app package - */ - public static boolean isBugleAppResource(final Uri uri) { - final String scheme = uri.getScheme(); - return TextUtils.equals(scheme, ContentResolver.SCHEME_ANDROID_RESOURCE); - } - - public static boolean isFileUri(final Uri uri) { - return uri != null && TextUtils.equals(uri.getScheme(), ContentResolver.SCHEME_FILE); - } - - /** - * Constructs an android.resource:// uri for the given resource id. - */ - public static Uri getUriForResourceId(final Context context, final int resId) { - return new Uri.Builder() - .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) - .authority(context.getPackageName()) - .appendPath(String.valueOf(resId)) - .build(); - } - - /** - * Returns whether the given Uri string is local. - */ - public static boolean isLocalUri(@NonNull final Uri uri) { - Assert.notNull(uri); - return SUPPORTED_SCHEME.contains(uri.getScheme()); - } - - private static final String MEDIA_STORE_URI_KLP = "com.android.providers.media.documents"; - - /** - * Check if a URI is from the MediaStore - */ - public static boolean isMediaStoreUri(final Uri uri) { - final String uriAuthority = uri.getAuthority(); - return TextUtils.equals(ContentResolver.SCHEME_CONTENT, uri.getScheme()) - && (TextUtils.equals(MediaStore.AUTHORITY, uriAuthority) || - // KK changed the media store authority name - TextUtils.equals(MEDIA_STORE_URI_KLP, uriAuthority)); - } - - /** - * Gets the size in bytes for the content uri. Currently we only support content in the - * scratch space. - */ - @DoesNotRunOnMainThread - public static long getContentSize(final Uri uri) { - Assert.isNotMainThread(); - if (isLocalResourceUri(uri)) { - ParcelFileDescriptor pfd = null; - try { - pfd = Factory.get().getApplicationContext() - .getContentResolver().openFileDescriptor(uri, "r"); - return Math.max(pfd.getStatSize(), 0); - } catch (final FileNotFoundException e) { - LogUtil.e(LogUtil.BUGLE_TAG, "Error getting content size", e); - } finally { - if (pfd != null) { - try { - pfd.close(); - } catch (final IOException e) { - // Do nothing. - } - } - } - } else { - Assert.fail("Unsupported uri type!"); - } - return 0; - } - - /** @return duration in milliseconds or 0 if not able to determine */ - public static int getMediaDurationMs(final Uri uri) { - final MediaMetadataRetrieverWrapper retriever = new MediaMetadataRetrieverWrapper(); - try { - retriever.setDataSource(uri); - return retriever.extractInteger(MediaMetadataRetriever.METADATA_KEY_DURATION, 0); - } catch (final IOException e) { - LogUtil.e(LogUtil.BUGLE_TAG, "Unable extract duration from media file: " + uri, e); - return 0; - } finally { - retriever.release(); - } - } - - /** - * Persist a piece of content from the given input stream, byte by byte to the scratch - * directory. - * @return the output Uri if the operation succeeded, or null if failed. - */ - @DoesNotRunOnMainThread - public static Uri persistContentToScratchSpace(final InputStream inputStream) { - final Context context = Factory.get().getApplicationContext(); - final Uri scratchSpaceUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(null); - return copyContent(context, inputStream, scratchSpaceUri); - } - - /** - * Persist a piece of content from the given sourceUri, byte by byte to the scratch - * directory. - * @return the output Uri if the operation succeeded, or null if failed. - */ - @DoesNotRunOnMainThread - public static Uri persistContentToScratchSpace(final Uri sourceUri) { - InputStream inputStream = null; - final Context context = Factory.get().getApplicationContext(); - try { - if (UriUtil.isLocalResourceUri(sourceUri)) { - inputStream = context.getContentResolver().openInputStream(sourceUri); - } else { - // The content is remote. Download it. - final URL url = new URL(sourceUri.toString()); - final URLConnection ucon = url.openConnection(); - inputStream = new BufferedInputStream(ucon.getInputStream()); - } - return persistContentToScratchSpace(inputStream); - } catch (final Exception ex) { - LogUtil.e(LogUtil.BUGLE_TAG, "Error while retrieving media ", ex); - return null; - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (final IOException e) { - LogUtil.e(LogUtil.BUGLE_TAG, "error trying to close the inputStream", e); - } - } - } - } - - /** - * Persist a piece of content from the given input stream, byte by byte to the specified - * directory. - * @return the output Uri if the operation succeeded, or null if failed. - */ - @DoesNotRunOnMainThread - public static Uri persistContent( - final InputStream inputStream, final File outputDir, final String contentType) { - if (!outputDir.exists() && !outputDir.mkdirs()) { - LogUtil.e(LogUtil.BUGLE_TAG, "Error creating " + outputDir.getAbsolutePath()); - return null; - } - - final Context context = Factory.get().getApplicationContext(); - try { - final Uri targetUri = Uri.fromFile(FileUtil.getNewFile(outputDir, contentType)); - return copyContent(context, inputStream, targetUri); - } catch (final IOException e) { - LogUtil.e(LogUtil.BUGLE_TAG, "Error creating file in " + outputDir.getAbsolutePath()); - return null; - } - } - - /** - * Persist a piece of content from the given sourceUri, byte by byte to the - * specified output directory. - * @return the output Uri if the operation succeeded, or null if failed. - */ - @DoesNotRunOnMainThread - public static Uri persistContent( - final Uri sourceUri, final File outputDir, final String contentType) { - InputStream inputStream = null; - final Context context = Factory.get().getApplicationContext(); - try { - if (UriUtil.isLocalResourceUri(sourceUri)) { - inputStream = context.getContentResolver().openInputStream(sourceUri); - } else { - // The content is remote. Download it. - final URL url = new URL(sourceUri.toString()); - final URLConnection ucon = url.openConnection(); - inputStream = new BufferedInputStream(ucon.getInputStream()); - } - return persistContent(inputStream, outputDir, contentType); - } catch (final Exception ex) { - LogUtil.e(LogUtil.BUGLE_TAG, "Error while retrieving media ", ex); - return null; - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (final IOException e) { - LogUtil.e(LogUtil.BUGLE_TAG, "error trying to close the inputStream", e); - } - } - } - } - - /** @return uri of target file, or null on error */ - @DoesNotRunOnMainThread - private static Uri copyContent( - final Context context, final InputStream inputStream, final Uri targetUri) { - Assert.isNotMainThread(); - OutputStream outputStream = null; - try { - outputStream = context.getContentResolver().openOutputStream(targetUri); - ByteStreams.copy(inputStream, outputStream); - } catch (final Exception ex) { - LogUtil.e(LogUtil.BUGLE_TAG, "Error while copying content ", ex); - return null; - } finally { - if (outputStream != null) { - try { - outputStream.flush(); - } catch (final IOException e) { - LogUtil.e(LogUtil.BUGLE_TAG, "error trying to flush the outputStream", e); - return null; - } finally { - try { - outputStream.close(); - } catch (final IOException e) { - // Do nothing. - } - } - } - } - return targetUri; - } - - public static boolean isSmsMmsUri(final Uri uri) { - return uri != null && SMS_MMS_SCHEMES.contains(uri.getScheme()); - } - - /** - * Extract recipient destinations from Uri of form - * SCHEME:destionation[,destination]?otherstuff - * where SCHEME is one of the supported sms/mms schemes. - * - * @param uri sms/mms uri - * @return recipient destinations or null - */ - public static String[] parseRecipientsFromSmsMmsUri(final Uri uri) { - if (!isSmsMmsUri(uri)) { - return null; - } - final String[] parts = uri.getSchemeSpecificPart().split("\\?"); - if (TextUtils.isEmpty(parts[0])) { - return null; - } - // replaceUnicodeDigits will replace digits typed in other languages (i.e. Egyptian) with - // the usual ascii equivalents. - return TextUtil.replaceUnicodeDigits(parts[0]).replace(';', ',').split(","); - } - - /** - * Return the length of the file to which contentUri refers - * - * @param contentUri URI for the file of which we want the length - * @return Length of the file or AssetFileDescriptor.UNKNOWN_LENGTH - */ - public static long getUriContentLength(final Uri contentUri) { - final Context context = Factory.get().getApplicationContext(); - AssetFileDescriptor afd = null; - try { - afd = context.getContentResolver().openAssetFileDescriptor(contentUri, "r"); - return afd.getLength(); - } catch (final FileNotFoundException e) { - LogUtil.w(LogUtil.BUGLE_TAG, "Failed to query length of " + contentUri); - } finally { - if (afd != null) { - try { - afd.close(); - } catch (final IOException e) { - LogUtil.w(LogUtil.BUGLE_TAG, "Failed to close afd for " + contentUri); - } - } - } - return AssetFileDescriptor.UNKNOWN_LENGTH; - } - - /** - * Download data from the given url to the given local file. - * @return true if the download was successful. - * - * TODO: Add retry/exponential backoff logic. - */ - @DoesNotRunOnMainThread - public static boolean downloadDataFromUrl(final String urlString, final File localFile) { - Assert.isNotMainThread(); - LogUtil.i(LogUtil.BUGLE_TAG, "Downloading from " + urlString + " to " + localFile); - try { - Files.createParentDirs(localFile); - final URL inUrl = new URL(urlString); - ByteStreams.copy(Resources.newInputStreamSupplier(inUrl), - Files.newOutputStreamSupplier(localFile)); - return true; - } catch (final IOException ex) { - LogUtil.e(LogUtil.BUGLE_TAG, "Error downloading from " + urlString, ex); - return false; - } - } - - /** @return string representation of URI or null if URI was null */ - public static String stringFromUri(final Uri uri) { - return uri == null ? null : uri.toString(); - } - - /** @return URI created from string or null if string was null or empty */ - public static Uri uriFromString(final String uriString) { - return TextUtils.isEmpty(uriString) ? null : Uri.parse(uriString); - } -} diff --git a/src/com/android/messaging/util/VersionUtil.java b/src/com/android/messaging/util/VersionUtil.java deleted file mode 100644 index b87aa55..0000000 --- a/src/com/android/messaging/util/VersionUtil.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; - -import java.util.Locale; - -public final class VersionUtil { - private static final Object sLock = new Object(); - private static VersionUtil sInstance; - private final String mSimpleVersionName; - private final int mVersionCode; - - public static VersionUtil getInstance(final Context context) { - synchronized (sLock) { - if (sInstance == null) { - sInstance = new VersionUtil(context); - } - } - return sInstance; - } - - private VersionUtil(final Context context) { - int versionCode; - try { - PackageInfo pi = context.getPackageManager().getPackageInfo( - context.getPackageName(), 0); - versionCode = pi.versionCode; - } catch (final NameNotFoundException exception) { - Assert.fail("couldn't get package info " + exception); - versionCode = -1; - } - mVersionCode = versionCode; - final int majorBuildNumber = versionCode / 1000; - // Use US locale to format version number so that other language characters don't - // show up in version string. - mSimpleVersionName = String.format(Locale.US, "%d.%d.%03d", - majorBuildNumber / 10000, - (majorBuildNumber / 1000) % 10, - majorBuildNumber % 1000); - } - - public int getVersionCode() { - return mVersionCode; - } - - public String getSimpleName() { - return mSimpleVersionName; - } -} diff --git a/src/com/android/messaging/util/WakeLockHelper.java b/src/com/android/messaging/util/WakeLockHelper.java deleted file mode 100644 index c9a9152..0000000 --- a/src/com/android/messaging/util/WakeLockHelper.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.content.Context; -import android.content.Intent; -import android.os.Debug; -import android.os.PowerManager; -import android.os.Process; - -import com.google.common.annotations.VisibleForTesting; - -/** - * Helper class used to manage wakelock state - */ -public class WakeLockHelper { - private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG; - private static final boolean VERBOSE = false; - - @VisibleForTesting - public static final String EXTRA_CALLING_PID = "pid"; - - private final Object mLock = new Object(); - private final String mWakeLockId; - private final int mMyPid; - - private PowerManager.WakeLock mWakeLock; - - public WakeLockHelper(final String wakeLockId) { - mWakeLockId = wakeLockId; - mMyPid = Process.myPid(); - } - - /** - * Acquire the wakelock - */ - public void acquire(final Context context, final Intent intent, final int opcode) { - synchronized (mLock) { - if (mWakeLock == null) { - if (VERBOSE) { - LogUtil.v(TAG, "initializing wakelock"); - } - final PowerManager pm = (PowerManager) - context.getSystemService(Context.POWER_SERVICE); - mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mWakeLockId); - } - } - if (VERBOSE) { - LogUtil.v(TAG, "acquiring " + mWakeLockId + " for opcode " + opcode); - } - mWakeLock.acquire(); - intent.putExtra(EXTRA_CALLING_PID, mMyPid); - } - - /** - * Check if wakelock held by this process - */ - public boolean isHeld(final Intent intent) { - final boolean respectWakeLock = (mMyPid == intent.getIntExtra(EXTRA_CALLING_PID, -1)); - return (respectWakeLock && mWakeLock.isHeld()); - } - - /** - * Ensure that wakelock is held by this process - */ - public boolean ensure(final Intent intent, final int opcode) { - final boolean respectWakeLock = (mMyPid == intent.getIntExtra(EXTRA_CALLING_PID, -1)); - if (VERBOSE) { - LogUtil.v(TAG, "WakeLockHelper.ensure Intent " + intent + " " - + intent.getAction() + " opcode: " + opcode - + " respectWakeLock " + respectWakeLock); - } - - if (respectWakeLock) { - final boolean isHeld = (respectWakeLock && isHeld(intent)); - if (!isHeld) { - LogUtil.e(TAG, "WakeLockHelper.ensure called " + intent + " " + intent.getAction() - + " opcode: " + opcode + " sWakeLock: " + mWakeLock + " isHeld: " - + ((mWakeLock == null) ? "(null)" : mWakeLock.isHeld())); - if (!Debug.isDebuggerConnected()) { - Assert.fail("WakeLock dropped prior to service starting"); - } - } - return true; - } - return false; - } - - /** - * Release wakelock (if it is held by this process) - */ - public void release(final Intent intent, final int opcode) { - final boolean respectWakeLock = (mMyPid == intent.getIntExtra(EXTRA_CALLING_PID, -1)); - if (respectWakeLock) { - try { - mWakeLock.release(); - } catch (final RuntimeException ex) { - LogUtil.e(TAG, "KeepAliveService.onHandleIntent exit crash " + intent + " " - + intent.getAction() + " opcode: " + opcode + " sWakeLock: " + mWakeLock - + " isHeld: " + ((mWakeLock == null) ? "(null)" : mWakeLock.isHeld())); - if (!Debug.isDebuggerConnected()) { - Assert.fail("WakeLock no longer held at end of handler"); - } - } - } - } -} diff --git a/src/com/android/messaging/util/YouTubeUtil.java b/src/com/android/messaging/util/YouTubeUtil.java deleted file mode 100644 index 203a666..0000000 --- a/src/com/android/messaging/util/YouTubeUtil.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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.android.messaging.util; - -import android.net.Uri; -import android.text.TextUtils; - -public class YouTubeUtil { - private static final String YOUTUBE_HOST_1 = "www.youtube.com"; - private static final String YOUTUBE_HOST_2 = "youtube.com"; - private static final String YOUTUBE_HOST_3 = "m.youtube.com"; - private static final String YOUTUBE_HOST_4 = "youtube.googleapis.com"; - private static final String YOUTUBE_HOST_5 = "youtu.be"; - - private static final String YOUTUBE_PATH_1 = "/watch"; - private static final String YOUTUBE_PATH_2 = "/embed/"; - private static final String YOUTUBE_PATH_3 = "/v/"; - private static final String YOUTUBE_PATH_4 = "/apiplayer"; - - public static final String YOUTUBE_STATIC_THUMBNAIL_PREFIX = "https://img.youtube.com/vi/"; - public static final String YOUTUBE_STATIC_THUMBNAIL_END = "/hqdefault.jpg"; - - public static String getYoutubePreviewImageLink(String urlString) { - // Types of youtube urls: - // 1.) http://www.youtube.com/watch?v=VIDEOID - // 2.) http://www.youtube.com/embed/VIDEOID - // 3.) http://www.youtube.com/v/VIDEOID - // 3a.) https://youtube.googleapis.com/v/VIDEOID - // 4.) http://www.youtube.com/apiplayer?video_id=VIDEO_ID - // 5.) http://youtu.be/VIDEOID - if (!urlString.startsWith("http")) { - // Apparently the url is not an RFC 2396 compliant uri without the port - urlString = "http://" + urlString; - } - final Uri uri = Uri.parse(urlString); - final String host = uri.getHost(); - if (YOUTUBE_HOST_1.equalsIgnoreCase(host) - || YOUTUBE_HOST_2.equalsIgnoreCase(host) - || YOUTUBE_HOST_3.equalsIgnoreCase(host) - || YOUTUBE_HOST_4.equalsIgnoreCase(host) - || YOUTUBE_HOST_5.equalsIgnoreCase(host)) { - final String videoId = getYouTubeVideoId(uri); - if (!TextUtils.isEmpty(videoId)) { - return YOUTUBE_STATIC_THUMBNAIL_PREFIX + videoId + YOUTUBE_STATIC_THUMBNAIL_END; - } - return null; - } - return null; - } - - private static String getYouTubeVideoId(Uri uri) { - final String urlPath = uri.getPath(); - - if (TextUtils.isEmpty(urlPath)) { - // There is no path so no need to continue. - return null; - } - // Case 1 - if (urlPath.startsWith(YOUTUBE_PATH_1)) { - return uri.getQueryParameter("v"); - } - // Case 2 - if (urlPath.startsWith(YOUTUBE_PATH_2)) { - return getVideoIdFromPath(YOUTUBE_PATH_2, urlPath); - } - // Case 3 - if (urlPath.startsWith(YOUTUBE_PATH_3)) { - return getVideoIdFromPath(YOUTUBE_PATH_3, urlPath); - } - // Case 4 - if (urlPath.startsWith(YOUTUBE_PATH_4)) { - return uri.getQueryParameter("video_id"); - } - // Case 5 - if (YOUTUBE_HOST_5.equalsIgnoreCase(uri.getHost())) { - return getVideoIdFromPath("/", urlPath); - } - return null; - } - - private static String getVideoIdFromPath(String prefixSubstring, String urlPath) { - return urlPath.substring(prefixSubstring.length()); - } - -} diff --git a/src/com/android/messaging/util/exif/ByteBufferInputStream.java b/src/com/android/messaging/util/exif/ByteBufferInputStream.java deleted file mode 100644 index 9db92ef..0000000 --- a/src/com/android/messaging/util/exif/ByteBufferInputStream.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2012 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.android.messaging.util.exif; - -import java.io.InputStream; -import java.nio.ByteBuffer; - -class ByteBufferInputStream extends InputStream { - - private final ByteBuffer mBuf; - - public ByteBufferInputStream(ByteBuffer buf) { - mBuf = buf; - } - - @Override - public int read() { - if (!mBuf.hasRemaining()) { - return -1; - } - return mBuf.get() & 0xFF; - } - - @Override - public int read(byte[] bytes, int off, int len) { - if (!mBuf.hasRemaining()) { - return -1; - } - - len = Math.min(len, mBuf.remaining()); - mBuf.get(bytes, off, len); - return len; - } -} diff --git a/src/com/android/messaging/util/exif/CountedDataInputStream.java b/src/com/android/messaging/util/exif/CountedDataInputStream.java deleted file mode 100644 index ce766d9..0000000 --- a/src/com/android/messaging/util/exif/CountedDataInputStream.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) 2012 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.android.messaging.util.exif; - -import java.io.EOFException; -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.charset.Charset; - -class CountedDataInputStream extends FilterInputStream { - - private int mCount = 0; - - // allocate a byte buffer for a long value; - private final byte mByteArray[] = new byte[8]; - private final ByteBuffer mByteBuffer = ByteBuffer.wrap(mByteArray); - - protected CountedDataInputStream(InputStream in) { - super(in); - } - - public int getReadByteCount() { - return mCount; - } - - @Override - public int read(byte[] b) throws IOException { - int r = in.read(b); - mCount += (r >= 0) ? r : 0; - return r; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - int r = in.read(b, off, len); - mCount += (r >= 0) ? r : 0; - return r; - } - - @Override - public int read() throws IOException { - int r = in.read(); - mCount += (r >= 0) ? 1 : 0; - return r; - } - - @Override - public long skip(long length) throws IOException { - long skip = in.skip(length); - mCount += skip; - return skip; - } - - public void skipOrThrow(long length) throws IOException { - if (skip(length) != length) { - throw new EOFException(); - } - } - - public void skipTo(long target) throws IOException { - long cur = mCount; - long diff = target - cur; - assert(diff >= 0); - skipOrThrow(diff); - } - - public void readOrThrow(byte[] b, int off, int len) throws IOException { - int r = read(b, off, len); - if (r != len) { - throw new EOFException(); - } - } - - public void readOrThrow(byte[] b) throws IOException { - readOrThrow(b, 0, b.length); - } - - public void setByteOrder(ByteOrder order) { - mByteBuffer.order(order); - } - - public ByteOrder getByteOrder() { - return mByteBuffer.order(); - } - - public short readShort() throws IOException { - readOrThrow(mByteArray, 0 , 2); - mByteBuffer.rewind(); - return mByteBuffer.getShort(); - } - - public int readUnsignedShort() throws IOException { - return readShort() & 0xffff; - } - - public int readInt() throws IOException { - readOrThrow(mByteArray, 0 , 4); - mByteBuffer.rewind(); - return mByteBuffer.getInt(); - } - - public long readUnsignedInt() throws IOException { - return readInt() & 0xffffffffL; - } - - public long readLong() throws IOException { - readOrThrow(mByteArray, 0 , 8); - mByteBuffer.rewind(); - return mByteBuffer.getLong(); - } - - public String readString(int n) throws IOException { - byte buf[] = new byte[n]; - readOrThrow(buf); - return new String(buf, "UTF8"); - } - - public String readString(int n, Charset charset) throws IOException { - byte buf[] = new byte[n]; - readOrThrow(buf); - return new String(buf, charset); - } -} diff --git a/src/com/android/messaging/util/exif/ExifData.java b/src/com/android/messaging/util/exif/ExifData.java deleted file mode 100644 index 77ba4e9..0000000 --- a/src/com/android/messaging/util/exif/ExifData.java +++ /dev/null @@ -1,349 +0,0 @@ -/* - * Copyright (C) 2012 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.android.messaging.util.exif; - -import android.util.Log; -import com.android.messaging.util.LogUtil; - -import java.io.UnsupportedEncodingException; -import java.nio.ByteOrder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * This class stores the EXIF header in IFDs according to the JPEG - * specification. It is the result produced by {@link ExifReader}. - * - * @see ExifReader - * @see IfdData - */ -class ExifData { - private static final String TAG = LogUtil.BUGLE_TAG; - private static final byte[] USER_COMMENT_ASCII = { - 0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00 - }; - private static final byte[] USER_COMMENT_JIS = { - 0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - private static final byte[] USER_COMMENT_UNICODE = { - 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00 - }; - - private final IfdData[] mIfdDatas = new IfdData[IfdId.TYPE_IFD_COUNT]; - private byte[] mThumbnail; - private final ArrayList<byte[]> mStripBytes = new ArrayList<byte[]>(); - private final ByteOrder mByteOrder; - - ExifData(ByteOrder order) { - mByteOrder = order; - } - - /** - * Gets the compressed thumbnail. Returns null if there is no compressed - * thumbnail. - * - * @see #hasCompressedThumbnail() - */ - protected byte[] getCompressedThumbnail() { - return mThumbnail; - } - - /** - * Sets the compressed thumbnail. - */ - protected void setCompressedThumbnail(byte[] thumbnail) { - mThumbnail = thumbnail; - } - - /** - * Returns true it this header contains a compressed thumbnail. - */ - protected boolean hasCompressedThumbnail() { - return mThumbnail != null; - } - - /** - * Adds an uncompressed strip. - */ - protected void setStripBytes(int index, byte[] strip) { - if (index < mStripBytes.size()) { - mStripBytes.set(index, strip); - } else { - for (int i = mStripBytes.size(); i < index; i++) { - mStripBytes.add(null); - } - mStripBytes.add(strip); - } - } - - /** - * Gets the strip count. - */ - protected int getStripCount() { - return mStripBytes.size(); - } - - /** - * Gets the strip at the specified index. - * - * @exceptions #IndexOutOfBoundException - */ - protected byte[] getStrip(int index) { - return mStripBytes.get(index); - } - - /** - * Returns true if this header contains uncompressed strip. - */ - protected boolean hasUncompressedStrip() { - return mStripBytes.size() != 0; - } - - /** - * Gets the byte order. - */ - protected ByteOrder getByteOrder() { - return mByteOrder; - } - - /** - * Returns the {@link IfdData} object corresponding to a given IFD if it - * exists or null. - */ - protected IfdData getIfdData(int ifdId) { - if (ExifTag.isValidIfd(ifdId)) { - return mIfdDatas[ifdId]; - } - return null; - } - - /** - * Adds IFD data. If IFD data of the same type already exists, it will be - * replaced by the new data. - */ - protected void addIfdData(IfdData data) { - mIfdDatas[data.getId()] = data; - } - - /** - * Returns the {@link IfdData} object corresponding to a given IFD or - * generates one if none exist. - */ - protected IfdData getOrCreateIfdData(int ifdId) { - IfdData ifdData = mIfdDatas[ifdId]; - if (ifdData == null) { - ifdData = new IfdData(ifdId); - mIfdDatas[ifdId] = ifdData; - } - return ifdData; - } - - /** - * Returns the tag with a given TID in the given IFD if the tag exists. - * Otherwise returns null. - */ - protected ExifTag getTag(short tag, int ifd) { - IfdData ifdData = mIfdDatas[ifd]; - return (ifdData == null) ? null : ifdData.getTag(tag); - } - - /** - * Adds the given ExifTag to its default IFD and returns an existing ExifTag - * with the same TID or null if none exist. - */ - protected ExifTag addTag(ExifTag tag) { - if (tag != null) { - int ifd = tag.getIfd(); - return addTag(tag, ifd); - } - return null; - } - - /** - * Adds the given ExifTag to the given IFD and returns an existing ExifTag - * with the same TID or null if none exist. - */ - protected ExifTag addTag(ExifTag tag, int ifdId) { - if (tag != null && ExifTag.isValidIfd(ifdId)) { - IfdData ifdData = getOrCreateIfdData(ifdId); - return ifdData.setTag(tag); - } - return null; - } - - protected void clearThumbnailAndStrips() { - mThumbnail = null; - mStripBytes.clear(); - } - - /** - * Removes the thumbnail and its related tags. IFD1 will be removed. - */ - protected void removeThumbnailData() { - clearThumbnailAndStrips(); - mIfdDatas[IfdId.TYPE_IFD_1] = null; - } - - /** - * Removes the tag with a given TID and IFD. - */ - protected void removeTag(short tagId, int ifdId) { - IfdData ifdData = mIfdDatas[ifdId]; - if (ifdData == null) { - return; - } - ifdData.removeTag(tagId); - } - - /** - * Decodes the user comment tag into string as specified in the EXIF - * standard. Returns null if decoding failed. - */ - protected String getUserComment() { - IfdData ifdData = mIfdDatas[IfdId.TYPE_IFD_0]; - if (ifdData == null) { - return null; - } - ExifTag tag = ifdData.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_USER_COMMENT)); - if (tag == null) { - return null; - } - if (tag.getComponentCount() < 8) { - return null; - } - - byte[] buf = new byte[tag.getComponentCount()]; - tag.getBytes(buf); - - byte[] code = new byte[8]; - System.arraycopy(buf, 0, code, 0, 8); - - try { - if (Arrays.equals(code, USER_COMMENT_ASCII)) { - return new String(buf, 8, buf.length - 8, "US-ASCII"); - } else if (Arrays.equals(code, USER_COMMENT_JIS)) { - return new String(buf, 8, buf.length - 8, "EUC-JP"); - } else if (Arrays.equals(code, USER_COMMENT_UNICODE)) { - return new String(buf, 8, buf.length - 8, "UTF-16"); - } else { - return null; - } - } catch (UnsupportedEncodingException e) { - Log.w(TAG, "Failed to decode the user comment"); - return null; - } - } - - /** - * Returns a list of all {@link ExifTag}s in the ExifData or null if there - * are none. - */ - protected List<ExifTag> getAllTags() { - ArrayList<ExifTag> ret = new ArrayList<ExifTag>(); - for (IfdData d : mIfdDatas) { - if (d != null) { - ExifTag[] tags = d.getAllTags(); - if (tags != null) { - for (ExifTag t : tags) { - ret.add(t); - } - } - } - } - if (ret.size() == 0) { - return null; - } - return ret; - } - - /** - * Returns a list of all {@link ExifTag}s in a given IFD or null if there - * are none. - */ - protected List<ExifTag> getAllTagsForIfd(int ifd) { - IfdData d = mIfdDatas[ifd]; - if (d == null) { - return null; - } - ExifTag[] tags = d.getAllTags(); - if (tags == null) { - return null; - } - ArrayList<ExifTag> ret = new ArrayList<ExifTag>(tags.length); - for (ExifTag t : tags) { - ret.add(t); - } - if (ret.size() == 0) { - return null; - } - return ret; - } - - /** - * Returns a list of all {@link ExifTag}s with a given TID or null if there - * are none. - */ - protected List<ExifTag> getAllTagsForTagId(short tag) { - ArrayList<ExifTag> ret = new ArrayList<ExifTag>(); - for (IfdData d : mIfdDatas) { - if (d != null) { - ExifTag t = d.getTag(tag); - if (t != null) { - ret.add(t); - } - } - } - if (ret.size() == 0) { - return null; - } - return ret; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (obj instanceof ExifData) { - ExifData data = (ExifData) obj; - if (data.mByteOrder != mByteOrder || - data.mStripBytes.size() != mStripBytes.size() || - !Arrays.equals(data.mThumbnail, mThumbnail)) { - return false; - } - for (int i = 0; i < mStripBytes.size(); i++) { - if (!Arrays.equals(data.mStripBytes.get(i), mStripBytes.get(i))) { - return false; - } - } - for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) { - IfdData ifd1 = data.getIfdData(i); - IfdData ifd2 = getIfdData(i); - if (ifd1 != ifd2 && ifd1 != null && !ifd1.equals(ifd2)) { - return false; - } - } - return true; - } - return false; - } - -} diff --git a/src/com/android/messaging/util/exif/ExifInterface.java b/src/com/android/messaging/util/exif/ExifInterface.java deleted file mode 100644 index b556748..0000000 --- a/src/com/android/messaging/util/exif/ExifInterface.java +++ /dev/null @@ -1,2448 +0,0 @@ -/* - * Copyright (C) 2013 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.android.messaging.util.exif; - -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.util.SparseIntArray; - -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.channels.FileChannel.MapMode; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.TimeZone; - -/** - * This class provides methods and constants for reading and writing jpeg file - * metadata. It contains a collection of ExifTags, and a collection of - * definitions for creating valid ExifTags. The collection of ExifTags can be - * updated by: reading new ones from a file, deleting or adding existing ones, - * or building new ExifTags from a tag definition. These ExifTags can be written - * to a valid jpeg image as exif metadata. - * <p> - * Each ExifTag has a tag ID (TID) and is stored in a specific image file - * directory (IFD) as specified by the exif standard. A tag definition can be - * looked up with a constant that is a combination of TID and IFD. This - * definition has information about the type, number of components, and valid - * IFDs for a tag. - * - * @see ExifTag - */ -public class ExifInterface { - public static final int TAG_NULL = -1; - public static final int IFD_NULL = -1; - public static final int DEFINITION_NULL = 0; - - /** - * Tag constants for Jeita EXIF 2.2 - */ - - // IFD 0 - public static final int TAG_IMAGE_WIDTH = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0100); - public static final int TAG_IMAGE_LENGTH = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0101); // Image height - public static final int TAG_BITS_PER_SAMPLE = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0102); - public static final int TAG_COMPRESSION = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0103); - public static final int TAG_PHOTOMETRIC_INTERPRETATION = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0106); - public static final int TAG_IMAGE_DESCRIPTION = - defineTag(IfdId.TYPE_IFD_0, (short) 0x010E); - public static final int TAG_MAKE = - defineTag(IfdId.TYPE_IFD_0, (short) 0x010F); - public static final int TAG_MODEL = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0110); - public static final int TAG_STRIP_OFFSETS = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0111); - public static final int TAG_ORIENTATION = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0112); - public static final int TAG_SAMPLES_PER_PIXEL = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0115); - public static final int TAG_ROWS_PER_STRIP = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0116); - public static final int TAG_STRIP_BYTE_COUNTS = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0117); - public static final int TAG_X_RESOLUTION = - defineTag(IfdId.TYPE_IFD_0, (short) 0x011A); - public static final int TAG_Y_RESOLUTION = - defineTag(IfdId.TYPE_IFD_0, (short) 0x011B); - public static final int TAG_PLANAR_CONFIGURATION = - defineTag(IfdId.TYPE_IFD_0, (short) 0x011C); - public static final int TAG_RESOLUTION_UNIT = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0128); - public static final int TAG_TRANSFER_FUNCTION = - defineTag(IfdId.TYPE_IFD_0, (short) 0x012D); - public static final int TAG_SOFTWARE = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0131); - public static final int TAG_DATE_TIME = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0132); - public static final int TAG_ARTIST = - defineTag(IfdId.TYPE_IFD_0, (short) 0x013B); - public static final int TAG_WHITE_POINT = - defineTag(IfdId.TYPE_IFD_0, (short) 0x013E); - public static final int TAG_PRIMARY_CHROMATICITIES = - defineTag(IfdId.TYPE_IFD_0, (short) 0x013F); - public static final int TAG_Y_CB_CR_COEFFICIENTS = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0211); - public static final int TAG_Y_CB_CR_SUB_SAMPLING = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0212); - public static final int TAG_Y_CB_CR_POSITIONING = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0213); - public static final int TAG_REFERENCE_BLACK_WHITE = - defineTag(IfdId.TYPE_IFD_0, (short) 0x0214); - public static final int TAG_COPYRIGHT = - defineTag(IfdId.TYPE_IFD_0, (short) 0x8298); - public static final int TAG_EXIF_IFD = - defineTag(IfdId.TYPE_IFD_0, (short) 0x8769); - public static final int TAG_GPS_IFD = - defineTag(IfdId.TYPE_IFD_0, (short) 0x8825); - // IFD 1 - public static final int TAG_JPEG_INTERCHANGE_FORMAT = - defineTag(IfdId.TYPE_IFD_1, (short) 0x0201); - public static final int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = - defineTag(IfdId.TYPE_IFD_1, (short) 0x0202); - // IFD Exif Tags - public static final int TAG_EXPOSURE_TIME = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x829A); - public static final int TAG_F_NUMBER = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x829D); - public static final int TAG_EXPOSURE_PROGRAM = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8822); - public static final int TAG_SPECTRAL_SENSITIVITY = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8824); - public static final int TAG_ISO_SPEED_RATINGS = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8827); - public static final int TAG_OECF = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8828); - public static final int TAG_EXIF_VERSION = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9000); - public static final int TAG_DATE_TIME_ORIGINAL = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9003); - public static final int TAG_DATE_TIME_DIGITIZED = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9004); - public static final int TAG_COMPONENTS_CONFIGURATION = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9101); - public static final int TAG_COMPRESSED_BITS_PER_PIXEL = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9102); - public static final int TAG_SHUTTER_SPEED_VALUE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9201); - public static final int TAG_APERTURE_VALUE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9202); - public static final int TAG_BRIGHTNESS_VALUE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9203); - public static final int TAG_EXPOSURE_BIAS_VALUE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9204); - public static final int TAG_MAX_APERTURE_VALUE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9205); - public static final int TAG_SUBJECT_DISTANCE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9206); - public static final int TAG_METERING_MODE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9207); - public static final int TAG_LIGHT_SOURCE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9208); - public static final int TAG_FLASH = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9209); - public static final int TAG_FOCAL_LENGTH = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x920A); - public static final int TAG_SUBJECT_AREA = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9214); - public static final int TAG_MAKER_NOTE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x927C); - public static final int TAG_USER_COMMENT = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9286); - public static final int TAG_SUB_SEC_TIME = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9290); - public static final int TAG_SUB_SEC_TIME_ORIGINAL = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9291); - public static final int TAG_SUB_SEC_TIME_DIGITIZED = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9292); - public static final int TAG_FLASHPIX_VERSION = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA000); - public static final int TAG_COLOR_SPACE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA001); - public static final int TAG_PIXEL_X_DIMENSION = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA002); - public static final int TAG_PIXEL_Y_DIMENSION = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA003); - public static final int TAG_RELATED_SOUND_FILE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA004); - public static final int TAG_INTEROPERABILITY_IFD = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA005); - public static final int TAG_FLASH_ENERGY = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20B); - public static final int TAG_SPATIAL_FREQUENCY_RESPONSE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20C); - public static final int TAG_FOCAL_PLANE_X_RESOLUTION = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20E); - public static final int TAG_FOCAL_PLANE_Y_RESOLUTION = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20F); - public static final int TAG_FOCAL_PLANE_RESOLUTION_UNIT = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA210); - public static final int TAG_SUBJECT_LOCATION = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA214); - public static final int TAG_EXPOSURE_INDEX = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA215); - public static final int TAG_SENSING_METHOD = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA217); - public static final int TAG_FILE_SOURCE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA300); - public static final int TAG_SCENE_TYPE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA301); - public static final int TAG_CFA_PATTERN = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA302); - public static final int TAG_CUSTOM_RENDERED = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA401); - public static final int TAG_EXPOSURE_MODE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA402); - public static final int TAG_WHITE_BALANCE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA403); - public static final int TAG_DIGITAL_ZOOM_RATIO = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA404); - public static final int TAG_FOCAL_LENGTH_IN_35_MM_FILE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA405); - public static final int TAG_SCENE_CAPTURE_TYPE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA406); - public static final int TAG_GAIN_CONTROL = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA407); - public static final int TAG_CONTRAST = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA408); - public static final int TAG_SATURATION = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA409); - public static final int TAG_SHARPNESS = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA40A); - public static final int TAG_DEVICE_SETTING_DESCRIPTION = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA40B); - public static final int TAG_SUBJECT_DISTANCE_RANGE = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA40C); - public static final int TAG_IMAGE_UNIQUE_ID = - defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA420); - // IFD GPS tags - public static final int TAG_GPS_VERSION_ID = - defineTag(IfdId.TYPE_IFD_GPS, (short) 0); - public static final int TAG_GPS_LATITUDE_REF = - defineTag(IfdId.TYPE_IFD_GPS, (short) 1); - public static final int TAG_GPS_LATITUDE = - defineTag(IfdId.TYPE_IFD_GPS, (short) 2); - public static final int TAG_GPS_LONGITUDE_REF = - defineTag(IfdId.TYPE_IFD_GPS, (short) 3); - public static final int TAG_GPS_LONGITUDE = - defineTag(IfdId.TYPE_IFD_GPS, (short) 4); - public static final int TAG_GPS_ALTITUDE_REF = - defineTag(IfdId.TYPE_IFD_GPS, (short) 5); - public static final int TAG_GPS_ALTITUDE = - defineTag(IfdId.TYPE_IFD_GPS, (short) 6); - public static final int TAG_GPS_TIME_STAMP = - defineTag(IfdId.TYPE_IFD_GPS, (short) 7); - public static final int TAG_GPS_SATTELLITES = - defineTag(IfdId.TYPE_IFD_GPS, (short) 8); - public static final int TAG_GPS_STATUS = - defineTag(IfdId.TYPE_IFD_GPS, (short) 9); - public static final int TAG_GPS_MEASURE_MODE = - defineTag(IfdId.TYPE_IFD_GPS, (short) 10); - public static final int TAG_GPS_DOP = - defineTag(IfdId.TYPE_IFD_GPS, (short) 11); - public static final int TAG_GPS_SPEED_REF = - defineTag(IfdId.TYPE_IFD_GPS, (short) 12); - public static final int TAG_GPS_SPEED = - defineTag(IfdId.TYPE_IFD_GPS, (short) 13); - public static final int TAG_GPS_TRACK_REF = - defineTag(IfdId.TYPE_IFD_GPS, (short) 14); - public static final int TAG_GPS_TRACK = - defineTag(IfdId.TYPE_IFD_GPS, (short) 15); - public static final int TAG_GPS_IMG_DIRECTION_REF = - defineTag(IfdId.TYPE_IFD_GPS, (short) 16); - public static final int TAG_GPS_IMG_DIRECTION = - defineTag(IfdId.TYPE_IFD_GPS, (short) 17); - public static final int TAG_GPS_MAP_DATUM = - defineTag(IfdId.TYPE_IFD_GPS, (short) 18); - public static final int TAG_GPS_DEST_LATITUDE_REF = - defineTag(IfdId.TYPE_IFD_GPS, (short) 19); - public static final int TAG_GPS_DEST_LATITUDE = - defineTag(IfdId.TYPE_IFD_GPS, (short) 20); - public static final int TAG_GPS_DEST_LONGITUDE_REF = - defineTag(IfdId.TYPE_IFD_GPS, (short) 21); - public static final int TAG_GPS_DEST_LONGITUDE = - defineTag(IfdId.TYPE_IFD_GPS, (short) 22); - public static final int TAG_GPS_DEST_BEARING_REF = - defineTag(IfdId.TYPE_IFD_GPS, (short) 23); - public static final int TAG_GPS_DEST_BEARING = - defineTag(IfdId.TYPE_IFD_GPS, (short) 24); - public static final int TAG_GPS_DEST_DISTANCE_REF = - defineTag(IfdId.TYPE_IFD_GPS, (short) 25); - public static final int TAG_GPS_DEST_DISTANCE = - defineTag(IfdId.TYPE_IFD_GPS, (short) 26); - public static final int TAG_GPS_PROCESSING_METHOD = - defineTag(IfdId.TYPE_IFD_GPS, (short) 27); - public static final int TAG_GPS_AREA_INFORMATION = - defineTag(IfdId.TYPE_IFD_GPS, (short) 28); - public static final int TAG_GPS_DATE_STAMP = - defineTag(IfdId.TYPE_IFD_GPS, (short) 29); - public static final int TAG_GPS_DIFFERENTIAL = - defineTag(IfdId.TYPE_IFD_GPS, (short) 30); - // IFD Interoperability tags - public static final int TAG_INTEROPERABILITY_INDEX = - defineTag(IfdId.TYPE_IFD_INTEROPERABILITY, (short) 1); - - /** - * Tags that contain offset markers. These are included in the banned - * defines. - */ - private static HashSet<Short> sOffsetTags = new HashSet<Short>(); - static { - sOffsetTags.add(getTrueTagKey(TAG_GPS_IFD)); - sOffsetTags.add(getTrueTagKey(TAG_EXIF_IFD)); - sOffsetTags.add(getTrueTagKey(TAG_JPEG_INTERCHANGE_FORMAT)); - sOffsetTags.add(getTrueTagKey(TAG_INTEROPERABILITY_IFD)); - sOffsetTags.add(getTrueTagKey(TAG_STRIP_OFFSETS)); - } - - /** - * Tags with definitions that cannot be overridden (banned defines). - */ - protected static HashSet<Short> sBannedDefines = new HashSet<Short>(sOffsetTags); - static { - sBannedDefines.add(getTrueTagKey(TAG_NULL)); - sBannedDefines.add(getTrueTagKey(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)); - sBannedDefines.add(getTrueTagKey(TAG_STRIP_BYTE_COUNTS)); - } - - /** - * Returns the constant representing a tag with a given TID and default IFD. - */ - public static int defineTag(int ifdId, short tagId) { - return (tagId & 0x0000ffff) | (ifdId << 16); - } - - /** - * Returns the TID for a tag constant. - */ - public static short getTrueTagKey(int tag) { - // Truncate - return (short) tag; - } - - /** - * Returns the default IFD for a tag constant. - */ - public static int getTrueIfd(int tag) { - return tag >>> 16; - } - - /** - * Constants for {@link TAG_ORIENTATION}. They can be interpreted as - * follows: - * <ul> - * <li>TOP_LEFT is the normal orientation.</li> - * <li>TOP_RIGHT is a left-right mirror.</li> - * <li>BOTTOM_LEFT is a 180 degree rotation.</li> - * <li>BOTTOM_RIGHT is a top-bottom mirror.</li> - * <li>LEFT_TOP is mirrored about the top-left<->bottom-right axis.</li> - * <li>RIGHT_TOP is a 90 degree clockwise rotation.</li> - * <li>LEFT_BOTTOM is mirrored about the top-right<->bottom-left axis.</li> - * <li>RIGHT_BOTTOM is a 270 degree clockwise rotation.</li> - * </ul> - */ - public static interface Orientation { - public static final short TOP_LEFT = 1; - public static final short TOP_RIGHT = 2; - public static final short BOTTOM_LEFT = 3; - public static final short BOTTOM_RIGHT = 4; - public static final short LEFT_TOP = 5; - public static final short RIGHT_TOP = 6; - public static final short LEFT_BOTTOM = 7; - public static final short RIGHT_BOTTOM = 8; - } - - /** - * Constants for {@link TAG_Y_CB_CR_POSITIONING} - */ - public static interface YCbCrPositioning { - public static final short CENTERED = 1; - public static final short CO_SITED = 2; - } - - /** - * Constants for {@link TAG_COMPRESSION} - */ - public static interface Compression { - public static final short UNCOMPRESSION = 1; - public static final short JPEG = 6; - } - - /** - * Constants for {@link TAG_RESOLUTION_UNIT} - */ - public static interface ResolutionUnit { - public static final short INCHES = 2; - public static final short CENTIMETERS = 3; - } - - /** - * Constants for {@link TAG_PHOTOMETRIC_INTERPRETATION} - */ - public static interface PhotometricInterpretation { - public static final short RGB = 2; - public static final short YCBCR = 6; - } - - /** - * Constants for {@link TAG_PLANAR_CONFIGURATION} - */ - public static interface PlanarConfiguration { - public static final short CHUNKY = 1; - public static final short PLANAR = 2; - } - - /** - * Constants for {@link TAG_EXPOSURE_PROGRAM} - */ - public static interface ExposureProgram { - public static final short NOT_DEFINED = 0; - public static final short MANUAL = 1; - public static final short NORMAL_PROGRAM = 2; - public static final short APERTURE_PRIORITY = 3; - public static final short SHUTTER_PRIORITY = 4; - public static final short CREATIVE_PROGRAM = 5; - public static final short ACTION_PROGRAM = 6; - public static final short PROTRAIT_MODE = 7; - public static final short LANDSCAPE_MODE = 8; - } - - /** - * Constants for {@link TAG_METERING_MODE} - */ - public static interface MeteringMode { - public static final short UNKNOWN = 0; - public static final short AVERAGE = 1; - public static final short CENTER_WEIGHTED_AVERAGE = 2; - public static final short SPOT = 3; - public static final short MULTISPOT = 4; - public static final short PATTERN = 5; - public static final short PARTAIL = 6; - public static final short OTHER = 255; - } - - /** - * Constants for {@link TAG_FLASH} As the definition in Jeita EXIF 2.2 - * standard, we can treat this constant as bitwise flag. - * <p> - * e.g. - * <p> - * short flash = FIRED | RETURN_STROBE_RETURN_LIGHT_DETECTED | - * MODE_AUTO_MODE - */ - public static interface Flash { - // LSB - public static final short DID_NOT_FIRED = 0; - public static final short FIRED = 1; - // 1st~2nd bits - public static final short RETURN_NO_STROBE_RETURN_DETECTION_FUNCTION = 0 << 1; - public static final short RETURN_STROBE_RETURN_LIGHT_NOT_DETECTED = 2 << 1; - public static final short RETURN_STROBE_RETURN_LIGHT_DETECTED = 3 << 1; - // 3rd~4th bits - public static final short MODE_UNKNOWN = 0 << 3; - public static final short MODE_COMPULSORY_FLASH_FIRING = 1 << 3; - public static final short MODE_COMPULSORY_FLASH_SUPPRESSION = 2 << 3; - public static final short MODE_AUTO_MODE = 3 << 3; - // 5th bit - public static final short FUNCTION_PRESENT = 0 << 5; - public static final short FUNCTION_NO_FUNCTION = 1 << 5; - // 6th bit - public static final short RED_EYE_REDUCTION_NO_OR_UNKNOWN = 0 << 6; - public static final short RED_EYE_REDUCTION_SUPPORT = 1 << 6; - } - - /** - * Constants for {@link TAG_COLOR_SPACE} - */ - public static interface ColorSpace { - public static final short SRGB = 1; - public static final short UNCALIBRATED = (short) 0xFFFF; - } - - /** - * Constants for {@link TAG_EXPOSURE_MODE} - */ - public static interface ExposureMode { - public static final short AUTO_EXPOSURE = 0; - public static final short MANUAL_EXPOSURE = 1; - public static final short AUTO_BRACKET = 2; - } - - /** - * Constants for {@link TAG_WHITE_BALANCE} - */ - public static interface WhiteBalance { - public static final short AUTO = 0; - public static final short MANUAL = 1; - } - - /** - * Constants for {@link TAG_SCENE_CAPTURE_TYPE} - */ - public static interface SceneCapture { - public static final short STANDARD = 0; - public static final short LANDSCAPE = 1; - public static final short PROTRAIT = 2; - public static final short NIGHT_SCENE = 3; - } - - /** - * Constants for {@link TAG_COMPONENTS_CONFIGURATION} - */ - public static interface ComponentsConfiguration { - public static final short NOT_EXIST = 0; - public static final short Y = 1; - public static final short CB = 2; - public static final short CR = 3; - public static final short R = 4; - public static final short G = 5; - public static final short B = 6; - } - - /** - * Constants for {@link TAG_LIGHT_SOURCE} - */ - public static interface LightSource { - public static final short UNKNOWN = 0; - public static final short DAYLIGHT = 1; - public static final short FLUORESCENT = 2; - public static final short TUNGSTEN = 3; - public static final short FLASH = 4; - public static final short FINE_WEATHER = 9; - public static final short CLOUDY_WEATHER = 10; - public static final short SHADE = 11; - public static final short DAYLIGHT_FLUORESCENT = 12; - public static final short DAY_WHITE_FLUORESCENT = 13; - public static final short COOL_WHITE_FLUORESCENT = 14; - public static final short WHITE_FLUORESCENT = 15; - public static final short STANDARD_LIGHT_A = 17; - public static final short STANDARD_LIGHT_B = 18; - public static final short STANDARD_LIGHT_C = 19; - public static final short D55 = 20; - public static final short D65 = 21; - public static final short D75 = 22; - public static final short D50 = 23; - public static final short ISO_STUDIO_TUNGSTEN = 24; - public static final short OTHER = 255; - } - - /** - * Constants for {@link TAG_SENSING_METHOD} - */ - public static interface SensingMethod { - public static final short NOT_DEFINED = 1; - public static final short ONE_CHIP_COLOR = 2; - public static final short TWO_CHIP_COLOR = 3; - public static final short THREE_CHIP_COLOR = 4; - public static final short COLOR_SEQUENTIAL_AREA = 5; - public static final short TRILINEAR = 7; - public static final short COLOR_SEQUENTIAL_LINEAR = 8; - } - - /** - * Constants for {@link TAG_FILE_SOURCE} - */ - public static interface FileSource { - public static final short DSC = 3; - } - - /** - * Constants for {@link TAG_SCENE_TYPE} - */ - public static interface SceneType { - public static final short DIRECT_PHOTOGRAPHED = 1; - } - - /** - * Constants for {@link TAG_GAIN_CONTROL} - */ - public static interface GainControl { - public static final short NONE = 0; - public static final short LOW_UP = 1; - public static final short HIGH_UP = 2; - public static final short LOW_DOWN = 3; - public static final short HIGH_DOWN = 4; - } - - /** - * Constants for {@link TAG_CONTRAST} - */ - public static interface Contrast { - public static final short NORMAL = 0; - public static final short SOFT = 1; - public static final short HARD = 2; - } - - /** - * Constants for {@link TAG_SATURATION} - */ - public static interface Saturation { - public static final short NORMAL = 0; - public static final short LOW = 1; - public static final short HIGH = 2; - } - - /** - * Constants for {@link TAG_SHARPNESS} - */ - public static interface Sharpness { - public static final short NORMAL = 0; - public static final short SOFT = 1; - public static final short HARD = 2; - } - - /** - * Constants for {@link TAG_SUBJECT_DISTANCE} - */ - public static interface SubjectDistance { - public static final short UNKNOWN = 0; - public static final short MACRO = 1; - public static final short CLOSE_VIEW = 2; - public static final short DISTANT_VIEW = 3; - } - - /** - * Constants for {@link TAG_GPS_LATITUDE_REF}, - * {@link TAG_GPS_DEST_LATITUDE_REF} - */ - public static interface GpsLatitudeRef { - public static final String NORTH = "N"; - public static final String SOUTH = "S"; - } - - /** - * Constants for {@link TAG_GPS_LONGITUDE_REF}, - * {@link TAG_GPS_DEST_LONGITUDE_REF} - */ - public static interface GpsLongitudeRef { - public static final String EAST = "E"; - public static final String WEST = "W"; - } - - /** - * Constants for {@link TAG_GPS_ALTITUDE_REF} - */ - public static interface GpsAltitudeRef { - public static final short SEA_LEVEL = 0; - public static final short SEA_LEVEL_NEGATIVE = 1; - } - - /** - * Constants for {@link TAG_GPS_STATUS} - */ - public static interface GpsStatus { - public static final String IN_PROGRESS = "A"; - public static final String INTEROPERABILITY = "V"; - } - - /** - * Constants for {@link TAG_GPS_MEASURE_MODE} - */ - public static interface GpsMeasureMode { - public static final String MODE_2_DIMENSIONAL = "2"; - public static final String MODE_3_DIMENSIONAL = "3"; - } - - /** - * Constants for {@link TAG_GPS_SPEED_REF}, - * {@link TAG_GPS_DEST_DISTANCE_REF} - */ - public static interface GpsSpeedRef { - public static final String KILOMETERS = "K"; - public static final String MILES = "M"; - public static final String KNOTS = "N"; - } - - /** - * Constants for {@link TAG_GPS_TRACK_REF}, - * {@link TAG_GPS_IMG_DIRECTION_REF}, {@link TAG_GPS_DEST_BEARING_REF} - */ - public static interface GpsTrackRef { - public static final String TRUE_DIRECTION = "T"; - public static final String MAGNETIC_DIRECTION = "M"; - } - - /** - * Constants for {@link TAG_GPS_DIFFERENTIAL} - */ - public static interface GpsDifferential { - public static final short WITHOUT_DIFFERENTIAL_CORRECTION = 0; - public static final short DIFFERENTIAL_CORRECTION_APPLIED = 1; - } - - private static final String NULL_ARGUMENT_STRING = "Argument is null"; - private ExifData mData = new ExifData(DEFAULT_BYTE_ORDER); - public static final ByteOrder DEFAULT_BYTE_ORDER = ByteOrder.BIG_ENDIAN; - - public ExifInterface() { - mGPSDateStampFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - } - - /** - * Reads the exif tags from a byte array, clearing this ExifInterface - * object's existing exif tags. - * - * @param jpeg a byte array containing a jpeg compressed image. - * @throws java.io.IOException - */ - public void readExif(byte[] jpeg) throws IOException { - readExif(new ByteArrayInputStream(jpeg)); - } - - /** - * Reads the exif tags from an InputStream, clearing this ExifInterface - * object's existing exif tags. - * - * @param inStream an InputStream containing a jpeg compressed image. - * @throws java.io.IOException - */ - public void readExif(InputStream inStream) throws IOException { - if (inStream == null) { - throw new IllegalArgumentException(NULL_ARGUMENT_STRING); - } - ExifData d = null; - try { - d = new ExifReader(this).read(inStream); - } catch (ExifInvalidFormatException e) { - throw new IOException("Invalid exif format : " + e); - } - mData = d; - } - - /** - * Reads the exif tags from a file, clearing this ExifInterface object's - * existing exif tags. - * - * @param inFileName a string representing the filepath to jpeg file. - * @throws java.io.FileNotFoundException - * @throws java.io.IOException - */ - public void readExif(String inFileName) throws FileNotFoundException, IOException { - if (inFileName == null) { - throw new IllegalArgumentException(NULL_ARGUMENT_STRING); - } - InputStream is = null; - try { - is = new BufferedInputStream(new FileInputStream(inFileName)); - readExif(is); - } catch (IOException e) { - closeSilently(is); - throw e; - } - is.close(); - } - - /** - * Sets the exif tags, clearing this ExifInterface object's existing exif - * tags. - * - * @param tags a collection of exif tags to set. - */ - public void setExif(Collection<ExifTag> tags) { - clearExif(); - setTags(tags); - } - - /** - * Clears this ExifInterface object's existing exif tags. - */ - public void clearExif() { - mData = new ExifData(DEFAULT_BYTE_ORDER); - } - - /** - * Writes the tags from this ExifInterface object into a jpeg image, - * removing prior exif tags. - * - * @param jpeg a byte array containing a jpeg compressed image. - * @param exifOutStream an OutputStream to which the jpeg image with added - * exif tags will be written. - * @throws java.io.IOException - */ - public void writeExif(byte[] jpeg, OutputStream exifOutStream) throws IOException { - if (jpeg == null || exifOutStream == null) { - throw new IllegalArgumentException(NULL_ARGUMENT_STRING); - } - OutputStream s = getExifWriterStream(exifOutStream); - s.write(jpeg, 0, jpeg.length); - s.flush(); - } - - /** - * Writes the tags from this ExifInterface object into a jpeg compressed - * bitmap, removing prior exif tags. - * - * @param bmap a bitmap to compress and write exif into. - * @param exifOutStream the OutputStream to which the jpeg image with added - * exif tags will be written. - * @throws java.io.IOException - */ - public void writeExif(Bitmap bmap, OutputStream exifOutStream) throws IOException { - if (bmap == null || exifOutStream == null) { - throw new IllegalArgumentException(NULL_ARGUMENT_STRING); - } - OutputStream s = getExifWriterStream(exifOutStream); - bmap.compress(Bitmap.CompressFormat.JPEG, 90, s); - s.flush(); - } - - /** - * Writes the tags from this ExifInterface object into a jpeg stream, - * removing prior exif tags. - * - * @param jpegStream an InputStream containing a jpeg compressed image. - * @param exifOutStream an OutputStream to which the jpeg image with added - * exif tags will be written. - * @throws java.io.IOException - */ - public void writeExif(InputStream jpegStream, OutputStream exifOutStream) throws IOException { - if (jpegStream == null || exifOutStream == null) { - throw new IllegalArgumentException(NULL_ARGUMENT_STRING); - } - OutputStream s = getExifWriterStream(exifOutStream); - doExifStreamIO(jpegStream, s); - s.flush(); - } - - /** - * Writes the tags from this ExifInterface object into a jpeg image, - * removing prior exif tags. - * - * @param jpeg a byte array containing a jpeg compressed image. - * @param exifOutFileName a String containing the filepath to which the jpeg - * image with added exif tags will be written. - * @throws java.io.FileNotFoundException - * @throws java.io.IOException - */ - public void writeExif(byte[] jpeg, String exifOutFileName) throws FileNotFoundException, - IOException { - if (jpeg == null || exifOutFileName == null) { - throw new IllegalArgumentException(NULL_ARGUMENT_STRING); - } - OutputStream s = null; - try { - s = getExifWriterStream(exifOutFileName); - s.write(jpeg, 0, jpeg.length); - s.flush(); - } catch (IOException e) { - closeSilently(s); - throw e; - } - s.close(); - } - - /** - * Writes the tags from this ExifInterface object into a jpeg compressed - * bitmap, removing prior exif tags. - * - * @param bmap a bitmap to compress and write exif into. - * @param exifOutFileName a String containing the filepath to which the jpeg - * image with added exif tags will be written. - * @throws java.io.FileNotFoundException - * @throws java.io.IOException - */ - public void writeExif(Bitmap bmap, String exifOutFileName) throws FileNotFoundException, - IOException { - if (bmap == null || exifOutFileName == null) { - throw new IllegalArgumentException(NULL_ARGUMENT_STRING); - } - OutputStream s = null; - try { - s = getExifWriterStream(exifOutFileName); - bmap.compress(Bitmap.CompressFormat.JPEG, 90, s); - s.flush(); - } catch (IOException e) { - closeSilently(s); - throw e; - } - s.close(); - } - - /** - * Writes the tags from this ExifInterface object into a jpeg stream, - * removing prior exif tags. - * - * @param jpegStream an InputStream containing a jpeg compressed image. - * @param exifOutFileName a String containing the filepath to which the jpeg - * image with added exif tags will be written. - * @throws java.io.FileNotFoundException - * @throws java.io.IOException - */ - public void writeExif(InputStream jpegStream, String exifOutFileName) - throws FileNotFoundException, IOException { - if (jpegStream == null || exifOutFileName == null) { - throw new IllegalArgumentException(NULL_ARGUMENT_STRING); - } - OutputStream s = null; - try { - s = getExifWriterStream(exifOutFileName); - doExifStreamIO(jpegStream, s); - s.flush(); - } catch (IOException e) { - closeSilently(s); - throw e; - } - s.close(); - } - - /** - * Writes the tags from this ExifInterface object into a jpeg file, removing - * prior exif tags. - * - * @param jpegFileName a String containing the filepath for a jpeg file. - * @param exifOutFileName a String containing the filepath to which the jpeg - * image with added exif tags will be written. - * @throws java.io.FileNotFoundException - * @throws java.io.IOException - */ - public void writeExif(String jpegFileName, String exifOutFileName) - throws FileNotFoundException, IOException { - if (jpegFileName == null || exifOutFileName == null) { - throw new IllegalArgumentException(NULL_ARGUMENT_STRING); - } - InputStream is = null; - try { - is = new FileInputStream(jpegFileName); - writeExif(is, exifOutFileName); - } catch (IOException e) { - closeSilently(is); - throw e; - } - is.close(); - } - - /** - * Wraps an OutputStream object with an ExifOutputStream. Exif tags in this - * ExifInterface object will be added to a jpeg image written to this - * stream, removing prior exif tags. Other methods of this ExifInterface - * object should not be called until the returned OutputStream has been - * closed. - * - * @param outStream an OutputStream to wrap. - * @return an OutputStream that wraps the outStream parameter, and adds exif - * metadata. A jpeg image should be written to this stream. - */ - public OutputStream getExifWriterStream(OutputStream outStream) { - if (outStream == null) { - throw new IllegalArgumentException(NULL_ARGUMENT_STRING); - } - ExifOutputStream eos = new ExifOutputStream(outStream, this); - eos.setExifData(mData); - return eos; - } - - /** - * Returns an OutputStream object that writes to a file. Exif tags in this - * ExifInterface object will be added to a jpeg image written to this - * stream, removing prior exif tags. Other methods of this ExifInterface - * object should not be called until the returned OutputStream has been - * closed. - * - * @param exifOutFileName an String containing a filepath for a jpeg file. - * @return an OutputStream that writes to the exifOutFileName file, and adds - * exif metadata. A jpeg image should be written to this stream. - * @throws java.io.FileNotFoundException - */ - public OutputStream getExifWriterStream(String exifOutFileName) throws FileNotFoundException { - if (exifOutFileName == null) { - throw new IllegalArgumentException(NULL_ARGUMENT_STRING); - } - OutputStream out = null; - try { - out = new FileOutputStream(exifOutFileName); - } catch (FileNotFoundException e) { - closeSilently(out); - throw e; - } - return getExifWriterStream(out); - } - - /** - * Attempts to do an in-place rewrite the exif metadata in a file for the - * given tags. If tags do not exist or do not have the same size as the - * existing exif tags, this method will fail. - * - * @param filename a String containing a filepath for a jpeg file with exif - * tags to rewrite. - * @param tags tags that will be written into the jpeg file over existing - * tags if possible. - * @return true if success, false if could not overwrite. If false, no - * changes are made to the file. - * @throws java.io.FileNotFoundException - * @throws java.io.IOException - */ - public boolean rewriteExif(String filename, Collection<ExifTag> tags) - throws FileNotFoundException, IOException { - RandomAccessFile file = null; - InputStream is = null; - boolean ret; - try { - File temp = new File(filename); - is = new BufferedInputStream(new FileInputStream(temp)); - - // Parse beginning of APP1 in exif to find size of exif header. - ExifParser parser = null; - try { - parser = ExifParser.parse(is, this); - } catch (ExifInvalidFormatException e) { - throw new IOException("Invalid exif format : ", e); - } - long exifSize = parser.getOffsetToExifEndFromSOF(); - - // Free up resources - is.close(); - is = null; - - // Open file for memory mapping. - file = new RandomAccessFile(temp, "rw"); - long fileLength = file.length(); - if (fileLength < exifSize) { - throw new IOException("Filesize changed during operation"); - } - - // Map only exif header into memory. - ByteBuffer buf = file.getChannel().map(MapMode.READ_WRITE, 0, exifSize); - - // Attempt to overwrite tag values without changing lengths (avoids - // file copy). - ret = rewriteExif(buf, tags); - } catch (IOException e) { - closeSilently(file); - throw e; - } finally { - closeSilently(is); - } - file.close(); - return ret; - } - - /** - * Attempts to do an in-place rewrite the exif metadata in a ByteBuffer for - * the given tags. If tags do not exist or do not have the same size as the - * existing exif tags, this method will fail. - * - * @param buf a ByteBuffer containing a jpeg file with existing exif tags to - * rewrite. - * @param tags tags that will be written into the jpeg ByteBuffer over - * existing tags if possible. - * @return true if success, false if could not overwrite. If false, no - * changes are made to the ByteBuffer. - * @throws java.io.IOException - */ - public boolean rewriteExif(ByteBuffer buf, Collection<ExifTag> tags) throws IOException { - ExifModifier mod = null; - try { - mod = new ExifModifier(buf, this); - for (ExifTag t : tags) { - mod.modifyTag(t); - } - return mod.commit(); - } catch (ExifInvalidFormatException e) { - throw new IOException("Invalid exif format : " + e); - } - } - - /** - * Attempts to do an in-place rewrite of the exif metadata. If this fails, - * fall back to overwriting file. This preserves tags that are not being - * rewritten. - * - * @param filename a String containing a filepath for a jpeg file. - * @param tags tags that will be written into the jpeg file over existing - * tags if possible. - * @throws java.io.FileNotFoundException - * @throws java.io.IOException - * @see #rewriteExif - */ - public void forceRewriteExif(String filename, Collection<ExifTag> tags) - throws FileNotFoundException, - IOException { - // Attempt in-place write - if (!rewriteExif(filename, tags)) { - // Fall back to doing a copy - ExifData tempData = mData; - mData = new ExifData(DEFAULT_BYTE_ORDER); - FileInputStream is = null; - ByteArrayOutputStream bytes = null; - try { - is = new FileInputStream(filename); - bytes = new ByteArrayOutputStream(); - doExifStreamIO(is, bytes); - byte[] imageBytes = bytes.toByteArray(); - readExif(imageBytes); - setTags(tags); - writeExif(imageBytes, filename); - } catch (IOException e) { - closeSilently(is); - throw e; - } finally { - is.close(); - // Prevent clobbering of mData - mData = tempData; - } - } - } - - /** - * Attempts to do an in-place rewrite of the exif metadata using the tags in - * this ExifInterface object. If this fails, fall back to overwriting file. - * This preserves tags that are not being rewritten. - * - * @param filename a String containing a filepath for a jpeg file. - * @throws java.io.FileNotFoundException - * @throws java.io.IOException - * @see #rewriteExif - */ - public void forceRewriteExif(String filename) throws FileNotFoundException, IOException { - forceRewriteExif(filename, getAllTags()); - } - - /** - * Get the exif tags in this ExifInterface object or null if none exist. - * - * @return a List of {@link ExifTag}s. - */ - public List<ExifTag> getAllTags() { - return mData.getAllTags(); - } - - /** - * Returns a list of ExifTags that share a TID (which can be obtained by - * calling {@link #getTrueTagKey} on a defined tag constant) or null if none - * exist. - * - * @param tagId a TID as defined in the exif standard (or with - * {@link #defineTag}). - * @return a List of {@link ExifTag}s. - */ - public List<ExifTag> getTagsForTagId(short tagId) { - return mData.getAllTagsForTagId(tagId); - } - - /** - * Returns a list of ExifTags that share an IFD (which can be obtained by - * calling {@link #getTrueIFD} on a defined tag constant) or null if none - * exist. - * - * @param ifdId an IFD as defined in the exif standard (or with - * {@link #defineTag}). - * @return a List of {@link ExifTag}s. - */ - public List<ExifTag> getTagsForIfdId(int ifdId) { - return mData.getAllTagsForIfd(ifdId); - } - - /** - * Gets an ExifTag for an IFD other than the tag's default. - * - * @see #getTag - */ - public ExifTag getTag(int tagId, int ifdId) { - if (!ExifTag.isValidIfd(ifdId)) { - return null; - } - return mData.getTag(getTrueTagKey(tagId), ifdId); - } - - /** - * Returns the ExifTag in that tag's default IFD for a defined tag constant - * or null if none exists. - * - * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. - * @return an {@link ExifTag} or null if none exists. - */ - public ExifTag getTag(int tagId) { - int ifdId = getDefinedTagDefaultIfd(tagId); - return getTag(tagId, ifdId); - } - - /** - * Gets a tag value for an IFD other than the tag's default. - * - * @see #getTagValue - */ - public Object getTagValue(int tagId, int ifdId) { - ExifTag t = getTag(tagId, ifdId); - return (t == null) ? null : t.getValue(); - } - - /** - * Returns the value of the ExifTag in that tag's default IFD for a defined - * tag constant or null if none exists or the value could not be cast into - * the return type. - * - * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. - * @return the value of the ExifTag or null if none exists. - */ - public Object getTagValue(int tagId) { - int ifdId = getDefinedTagDefaultIfd(tagId); - return getTagValue(tagId, ifdId); - } - - /* - * Getter methods that are similar to getTagValue. Null is returned if the - * tag value cannot be cast into the return type. - */ - - /** - * @see #getTagValue - */ - public String getTagStringValue(int tagId, int ifdId) { - ExifTag t = getTag(tagId, ifdId); - if (t == null) { - return null; - } - return t.getValueAsString(); - } - - /** - * @see #getTagValue - */ - public String getTagStringValue(int tagId) { - int ifdId = getDefinedTagDefaultIfd(tagId); - return getTagStringValue(tagId, ifdId); - } - - /** - * @see #getTagValue - */ - public Long getTagLongValue(int tagId, int ifdId) { - long[] l = getTagLongValues(tagId, ifdId); - if (l == null || l.length <= 0) { - return null; - } - return new Long(l[0]); - } - - /** - * @see #getTagValue - */ - public Long getTagLongValue(int tagId) { - int ifdId = getDefinedTagDefaultIfd(tagId); - return getTagLongValue(tagId, ifdId); - } - - /** - * @see #getTagValue - */ - public Integer getTagIntValue(int tagId, int ifdId) { - int[] l = getTagIntValues(tagId, ifdId); - if (l == null || l.length <= 0) { - return null; - } - return new Integer(l[0]); - } - - /** - * @see #getTagValue - */ - public Integer getTagIntValue(int tagId) { - int ifdId = getDefinedTagDefaultIfd(tagId); - return getTagIntValue(tagId, ifdId); - } - - /** - * @see #getTagValue - */ - public Byte getTagByteValue(int tagId, int ifdId) { - byte[] l = getTagByteValues(tagId, ifdId); - if (l == null || l.length <= 0) { - return null; - } - return new Byte(l[0]); - } - - /** - * @see #getTagValue - */ - public Byte getTagByteValue(int tagId) { - int ifdId = getDefinedTagDefaultIfd(tagId); - return getTagByteValue(tagId, ifdId); - } - - /** - * @see #getTagValue - */ - public Rational getTagRationalValue(int tagId, int ifdId) { - Rational[] l = getTagRationalValues(tagId, ifdId); - if (l == null || l.length == 0) { - return null; - } - return new Rational(l[0]); - } - - /** - * @see #getTagValue - */ - public Rational getTagRationalValue(int tagId) { - int ifdId = getDefinedTagDefaultIfd(tagId); - return getTagRationalValue(tagId, ifdId); - } - - /** - * @see #getTagValue - */ - public long[] getTagLongValues(int tagId, int ifdId) { - ExifTag t = getTag(tagId, ifdId); - if (t == null) { - return null; - } - return t.getValueAsLongs(); - } - - /** - * @see #getTagValue - */ - public long[] getTagLongValues(int tagId) { - int ifdId = getDefinedTagDefaultIfd(tagId); - return getTagLongValues(tagId, ifdId); - } - - /** - * @see #getTagValue - */ - public int[] getTagIntValues(int tagId, int ifdId) { - ExifTag t = getTag(tagId, ifdId); - if (t == null) { - return null; - } - return t.getValueAsInts(); - } - - /** - * @see #getTagValue - */ - public int[] getTagIntValues(int tagId) { - int ifdId = getDefinedTagDefaultIfd(tagId); - return getTagIntValues(tagId, ifdId); - } - - /** - * @see #getTagValue - */ - public byte[] getTagByteValues(int tagId, int ifdId) { - ExifTag t = getTag(tagId, ifdId); - if (t == null) { - return null; - } - return t.getValueAsBytes(); - } - - /** - * @see #getTagValue - */ - public byte[] getTagByteValues(int tagId) { - int ifdId = getDefinedTagDefaultIfd(tagId); - return getTagByteValues(tagId, ifdId); - } - - /** - * @see #getTagValue - */ - public Rational[] getTagRationalValues(int tagId, int ifdId) { - ExifTag t = getTag(tagId, ifdId); - if (t == null) { - return null; - } - return t.getValueAsRationals(); - } - - /** - * @see #getTagValue - */ - public Rational[] getTagRationalValues(int tagId) { - int ifdId = getDefinedTagDefaultIfd(tagId); - return getTagRationalValues(tagId, ifdId); - } - - /** - * Checks whether a tag has a defined number of elements. - * - * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. - * @return true if the tag has a defined number of elements. - */ - public boolean isTagCountDefined(int tagId) { - int info = getTagInfo().get(tagId); - // No value in info can be zero, as all tags have a non-zero type - if (info == 0) { - return false; - } - return getComponentCountFromInfo(info) != ExifTag.SIZE_UNDEFINED; - } - - /** - * Gets the defined number of elements for a tag. - * - * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. - * @return the number of elements or {@link ExifTag#SIZE_UNDEFINED} if the - * tag or the number of elements is not defined. - */ - public int getDefinedTagCount(int tagId) { - int info = getTagInfo().get(tagId); - if (info == 0) { - return ExifTag.SIZE_UNDEFINED; - } - return getComponentCountFromInfo(info); - } - - /** - * Gets the number of elements for an ExifTag in a given IFD. - * - * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. - * @param ifdId the IFD containing the ExifTag to check. - * @return the number of elements in the ExifTag, if the tag's size is - * undefined this will return the actual number of elements that is - * in the ExifTag's value. - */ - public int getActualTagCount(int tagId, int ifdId) { - ExifTag t = getTag(tagId, ifdId); - if (t == null) { - return 0; - } - return t.getComponentCount(); - } - - /** - * Gets the default IFD for a tag. - * - * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. - * @return the default IFD for a tag definition or {@link #IFD_NULL} if no - * definition exists. - */ - public int getDefinedTagDefaultIfd(int tagId) { - int info = getTagInfo().get(tagId); - if (info == DEFINITION_NULL) { - return IFD_NULL; - } - return getTrueIfd(tagId); - } - - /** - * Gets the defined type for a tag. - * - * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. - * @return the type. - * @see ExifTag#getDataType() - */ - public short getDefinedTagType(int tagId) { - int info = getTagInfo().get(tagId); - if (info == 0) { - return -1; - } - return getTypeFromInfo(info); - } - - /** - * Returns true if tag TID is one of the following: {@link TAG_EXIF_IFD}, - * {@link TAG_GPS_IFD}, {@link TAG_JPEG_INTERCHANGE_FORMAT}, - * {@link TAG_STRIP_OFFSETS}, {@link TAG_INTEROPERABILITY_IFD} - * <p> - * Note: defining tags with these TID's is disallowed. - * - * @param tag a tag's TID (can be obtained from a defined tag constant with - * {@link #getTrueTagKey}). - * @return true if the TID is that of an offset tag. - */ - protected static boolean isOffsetTag(short tag) { - return sOffsetTags.contains(tag); - } - - /** - * Creates a tag for a defined tag constant in a given IFD if that IFD is - * allowed for the tag. This method will fail anytime the appropriate - * {@link ExifTag#setValue} for this tag's datatype would fail. - * - * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. - * @param ifdId the IFD that the tag should be in. - * @param val the value of the tag to set. - * @return an ExifTag object or null if one could not be constructed. - * @see #buildTag - */ - public ExifTag buildTag(int tagId, int ifdId, Object val) { - int info = getTagInfo().get(tagId); - if (info == 0 || val == null) { - return null; - } - short type = getTypeFromInfo(info); - int definedCount = getComponentCountFromInfo(info); - boolean hasDefinedCount = (definedCount != ExifTag.SIZE_UNDEFINED); - if (!ExifInterface.isIfdAllowed(info, ifdId)) { - return null; - } - ExifTag t = new ExifTag(getTrueTagKey(tagId), type, definedCount, ifdId, hasDefinedCount); - if (!t.setValue(val)) { - return null; - } - return t; - } - - /** - * Creates a tag for a defined tag constant in the tag's default IFD. - * - * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. - * @param val the tag's value. - * @return an ExifTag object. - */ - public ExifTag buildTag(int tagId, Object val) { - int ifdId = getTrueIfd(tagId); - return buildTag(tagId, ifdId, val); - } - - protected ExifTag buildUninitializedTag(int tagId) { - int info = getTagInfo().get(tagId); - if (info == 0) { - return null; - } - short type = getTypeFromInfo(info); - int definedCount = getComponentCountFromInfo(info); - boolean hasDefinedCount = (definedCount != ExifTag.SIZE_UNDEFINED); - int ifdId = getTrueIfd(tagId); - ExifTag t = new ExifTag(getTrueTagKey(tagId), type, definedCount, ifdId, hasDefinedCount); - return t; - } - - /** - * Sets the value of an ExifTag if it exists in the given IFD. The value - * must be the correct type and length for that ExifTag. - * - * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. - * @param ifdId the IFD that the ExifTag is in. - * @param val the value to set. - * @return true if success, false if the ExifTag doesn't exist or the value - * is the wrong type/length. - * @see #setTagValue - */ - public boolean setTagValue(int tagId, int ifdId, Object val) { - ExifTag t = getTag(tagId, ifdId); - if (t == null) { - return false; - } - return t.setValue(val); - } - - /** - * Sets the value of an ExifTag if it exists it's default IFD. The value - * must be the correct type and length for that ExifTag. - * - * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. - * @param val the value to set. - * @return true if success, false if the ExifTag doesn't exist or the value - * is the wrong type/length. - */ - public boolean setTagValue(int tagId, Object val) { - int ifdId = getDefinedTagDefaultIfd(tagId); - return setTagValue(tagId, ifdId, val); - } - - /** - * Puts an ExifTag into this ExifInterface object's tags, removing a - * previous ExifTag with the same TID and IFD. The IFD it is put into will - * be the one the tag was created with in {@link #buildTag}. - * - * @param tag an ExifTag to put into this ExifInterface's tags. - * @return the previous ExifTag with the same TID and IFD or null if none - * exists. - */ - public ExifTag setTag(ExifTag tag) { - return mData.addTag(tag); - } - - /** - * Puts a collection of ExifTags into this ExifInterface objects's tags. Any - * previous ExifTags with the same TID and IFDs will be removed. - * - * @param tags a Collection of ExifTags. - * @see #setTag - */ - public void setTags(Collection<ExifTag> tags) { - for (ExifTag t : tags) { - setTag(t); - } - } - - /** - * Removes the ExifTag for a tag constant from the given IFD. - * - * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. - * @param ifdId the IFD of the ExifTag to remove. - */ - public void deleteTag(int tagId, int ifdId) { - mData.removeTag(getTrueTagKey(tagId), ifdId); - } - - /** - * Removes the ExifTag for a tag constant from that tag's default IFD. - * - * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. - */ - public void deleteTag(int tagId) { - int ifdId = getDefinedTagDefaultIfd(tagId); - deleteTag(tagId, ifdId); - } - - /** - * Creates a new tag definition in this ExifInterface object for a given TID - * and default IFD. Creating a definition with the same TID and default IFD - * as a previous definition will override it. - * - * @param tagId the TID for the tag. - * @param defaultIfd the default IFD for the tag. - * @param tagType the type of the tag (see {@link ExifTag#getDataType()}). - * @param defaultComponentCount the number of elements of this tag's type in - * the tags value. - * @param allowedIfds the IFD's this tag is allowed to be put in. - * @return the defined tag constant (e.g. {@link #TAG_IMAGE_WIDTH}) or - * {@link #TAG_NULL} if the definition could not be made. - */ - public int setTagDefinition(short tagId, int defaultIfd, short tagType, - short defaultComponentCount, int[] allowedIfds) { - if (sBannedDefines.contains(tagId)) { - return TAG_NULL; - } - if (ExifTag.isValidType(tagType) && ExifTag.isValidIfd(defaultIfd)) { - int tagDef = defineTag(defaultIfd, tagId); - if (tagDef == TAG_NULL) { - return TAG_NULL; - } - int[] otherDefs = getTagDefinitionsForTagId(tagId); - SparseIntArray infos = getTagInfo(); - // Make sure defaultIfd is in allowedIfds - boolean defaultCheck = false; - for (int i : allowedIfds) { - if (defaultIfd == i) { - defaultCheck = true; - } - if (!ExifTag.isValidIfd(i)) { - return TAG_NULL; - } - } - if (!defaultCheck) { - return TAG_NULL; - } - - int ifdFlags = getFlagsFromAllowedIfds(allowedIfds); - // Make sure no identical tags can exist in allowedIfds - if (otherDefs != null) { - for (int def : otherDefs) { - int tagInfo = infos.get(def); - int allowedFlags = getAllowedIfdFlagsFromInfo(tagInfo); - if ((ifdFlags & allowedFlags) != 0) { - return TAG_NULL; - } - } - } - getTagInfo().put(tagDef, ifdFlags << 24 | (tagType << 16) | defaultComponentCount); - return tagDef; - } - return TAG_NULL; - } - - protected int getTagDefinition(short tagId, int defaultIfd) { - return getTagInfo().get(defineTag(defaultIfd, tagId)); - } - - protected int[] getTagDefinitionsForTagId(short tagId) { - int[] ifds = IfdData.getIfds(); - int[] defs = new int[ifds.length]; - int counter = 0; - SparseIntArray infos = getTagInfo(); - for (int i : ifds) { - int def = defineTag(i, tagId); - if (infos.get(def) != DEFINITION_NULL) { - defs[counter++] = def; - } - } - if (counter == 0) { - return null; - } - - return Arrays.copyOfRange(defs, 0, counter); - } - - protected int getTagDefinitionForTag(ExifTag tag) { - short type = tag.getDataType(); - int count = tag.getComponentCount(); - int ifd = tag.getIfd(); - return getTagDefinitionForTag(tag.getTagId(), type, count, ifd); - } - - protected int getTagDefinitionForTag(short tagId, short type, int count, int ifd) { - int[] defs = getTagDefinitionsForTagId(tagId); - if (defs == null) { - return TAG_NULL; - } - SparseIntArray infos = getTagInfo(); - int ret = TAG_NULL; - for (int i : defs) { - int info = infos.get(i); - short defType = getTypeFromInfo(info); - int defCount = getComponentCountFromInfo(info); - int[] defIfds = getAllowedIfdsFromInfo(info); - boolean validIfd = false; - for (int j : defIfds) { - if (j == ifd) { - validIfd = true; - break; - } - } - if (validIfd && type == defType - && (count == defCount || defCount == ExifTag.SIZE_UNDEFINED)) { - ret = i; - break; - } - } - return ret; - } - - /** - * Removes a tag definition for given defined tag constant. - * - * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. - */ - public void removeTagDefinition(int tagId) { - getTagInfo().delete(tagId); - } - - /** - * Resets tag definitions to the default ones. - */ - public void resetTagDefinitions() { - mTagInfo = null; - } - - /** - * Returns the thumbnail from IFD1 as a bitmap, or null if none exists. - * - * @return the thumbnail as a bitmap. - */ - public Bitmap getThumbnailBitmap() { - if (mData.hasCompressedThumbnail()) { - byte[] thumb = mData.getCompressedThumbnail(); - return BitmapFactory.decodeByteArray(thumb, 0, thumb.length); - } else if (mData.hasUncompressedStrip()) { - // TODO: implement uncompressed - } - return null; - } - - /** - * Returns the thumbnail from IFD1 as a byte array, or null if none exists. - * The bytes may either be an uncompressed strip as specified in the exif - * standard or a jpeg compressed image. - * - * @return the thumbnail as a byte array. - */ - public byte[] getThumbnailBytes() { - if (mData.hasCompressedThumbnail()) { - return mData.getCompressedThumbnail(); - } else if (mData.hasUncompressedStrip()) { - // TODO: implement this - } - return null; - } - - /** - * Returns the thumbnail if it is jpeg compressed, or null if none exists. - * - * @return the thumbnail as a byte array. - */ - public byte[] getThumbnail() { - return mData.getCompressedThumbnail(); - } - - /** - * Check if thumbnail is compressed. - * - * @return true if the thumbnail is compressed. - */ - public boolean isThumbnailCompressed() { - return mData.hasCompressedThumbnail(); - } - - /** - * Check if thumbnail exists. - * - * @return true if a compressed thumbnail exists. - */ - public boolean hasThumbnail() { - // TODO: add back in uncompressed strip - return mData.hasCompressedThumbnail(); - } - - // TODO: uncompressed thumbnail setters - - /** - * Sets the thumbnail to be a jpeg compressed image. Clears any prior - * thumbnail. - * - * @param thumb a byte array containing a jpeg compressed image. - * @return true if the thumbnail was set. - */ - public boolean setCompressedThumbnail(byte[] thumb) { - mData.clearThumbnailAndStrips(); - mData.setCompressedThumbnail(thumb); - return true; - } - - /** - * Sets the thumbnail to be a jpeg compressed bitmap. Clears any prior - * thumbnail. - * - * @param thumb a bitmap to compress to a jpeg thumbnail. - * @return true if the thumbnail was set. - */ - public boolean setCompressedThumbnail(Bitmap thumb) { - ByteArrayOutputStream thumbnail = new ByteArrayOutputStream(); - if (!thumb.compress(Bitmap.CompressFormat.JPEG, 90, thumbnail)) { - return false; - } - return setCompressedThumbnail(thumbnail.toByteArray()); - } - - /** - * Clears the compressed thumbnail if it exists. - */ - public void removeCompressedThumbnail() { - mData.setCompressedThumbnail(null); - } - - // Convenience methods: - - /** - * Decodes the user comment tag into string as specified in the EXIF - * standard. Returns null if decoding failed. - */ - public String getUserComment() { - return mData.getUserComment(); - } - - /** - * Returns the Orientation ExifTag value for a given number of degrees. - * - * @param degrees the amount an image is rotated in degrees. - */ - public static short getOrientationValueForRotation(int degrees) { - degrees %= 360; - if (degrees < 0) { - degrees += 360; - } - if (degrees < 90) { - return Orientation.TOP_LEFT; // 0 degrees - } else if (degrees < 180) { - return Orientation.RIGHT_TOP; // 90 degrees cw - } else if (degrees < 270) { - return Orientation.BOTTOM_LEFT; // 180 degrees - } else { - return Orientation.RIGHT_BOTTOM; // 270 degrees cw - } - } - - /** - * Returns the rotation degrees corresponding to an ExifTag Orientation - * value. - * - * @param orientation the ExifTag Orientation value. - */ - public static int getRotationForOrientationValue(short orientation) { - switch (orientation) { - case Orientation.TOP_LEFT: - return 0; - case Orientation.RIGHT_TOP: - return 90; - case Orientation.BOTTOM_LEFT: - return 180; - case Orientation.RIGHT_BOTTOM: - return 270; - default: - return 0; - } - } - - public static OrientationParams getOrientationParams(int orientation) { - OrientationParams params = new OrientationParams(); - switch (orientation) { - case Orientation.TOP_RIGHT: // Flip horizontal - params.scaleX = -1; - break; - case Orientation.BOTTOM_RIGHT: // Flip vertical - params.scaleY = -1; - break; - case Orientation.BOTTOM_LEFT: // Rotate 180 - params.rotation = 180; - break; - case Orientation.RIGHT_BOTTOM: // Rotate 270 - params.rotation = 270; - params.invertDimensions = true; - break; - case Orientation.RIGHT_TOP: // Rotate 90 - params.rotation = 90; - params.invertDimensions = true; - break; - case Orientation.LEFT_TOP: // Transpose - params.rotation = 90; - params.scaleX = -1; - params.invertDimensions = true; - break; - case Orientation.LEFT_BOTTOM: // Transverse - params.rotation = 270; - params.scaleX = -1; - params.invertDimensions = true; - break; - } - return params; - } - - public static class OrientationParams { - public int rotation = 0; - public int scaleX = 1; - public int scaleY = 1; - public boolean invertDimensions = false; - } - - /** - * Gets the double representation of the GPS latitude or longitude - * coordinate. - * - * @param coordinate an array of 3 Rationals representing the degrees, - * minutes, and seconds of the GPS location as defined in the - * exif specification. - * @param reference a GPS reference reperesented by a String containing "N", - * "S", "E", or "W". - * @return the GPS coordinate represented as degrees + minutes/60 + - * seconds/3600 - */ - public static double convertLatOrLongToDouble(Rational[] coordinate, String reference) { - try { - double degrees = coordinate[0].toDouble(); - double minutes = coordinate[1].toDouble(); - double seconds = coordinate[2].toDouble(); - double result = degrees + minutes / 60.0 + seconds / 3600.0; - if ((reference.equals("S") || reference.equals("W"))) { - return -result; - } - return result; - } catch (ArrayIndexOutOfBoundsException e) { - throw new IllegalArgumentException(); - } - } - - /** - * Gets the GPS latitude and longitude as a pair of doubles from this - * ExifInterface object's tags, or null if the necessary tags do not exist. - * - * @return an array of 2 doubles containing the latitude, and longitude - * respectively. - * @see #convertLatOrLongToDouble - */ - public double[] getLatLongAsDoubles() { - Rational[] latitude = getTagRationalValues(TAG_GPS_LATITUDE); - String latitudeRef = getTagStringValue(TAG_GPS_LATITUDE_REF); - Rational[] longitude = getTagRationalValues(TAG_GPS_LONGITUDE); - String longitudeRef = getTagStringValue(TAG_GPS_LONGITUDE_REF); - if (latitude == null || longitude == null || latitudeRef == null || longitudeRef == null - || latitude.length < 3 || longitude.length < 3) { - return null; - } - double[] latLon = new double[2]; - latLon[0] = convertLatOrLongToDouble(latitude, latitudeRef); - latLon[1] = convertLatOrLongToDouble(longitude, longitudeRef); - return latLon; - } - - private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd"; - private static final String DATETIME_FORMAT_STR = "yyyy:MM:dd kk:mm:ss"; - private final DateFormat mDateTimeStampFormat = new SimpleDateFormat(DATETIME_FORMAT_STR); - private final DateFormat mGPSDateStampFormat = new SimpleDateFormat(GPS_DATE_FORMAT_STR); - private final Calendar mGPSTimeStampCalendar = Calendar - .getInstance(TimeZone.getTimeZone("UTC")); - - /** - * Creates, formats, and sets the DateTimeStamp tag for one of: - * {@link #TAG_DATE_TIME}, {@link #TAG_DATE_TIME_DIGITIZED}, - * {@link #TAG_DATE_TIME_ORIGINAL}. - * - * @param tagId one of the DateTimeStamp tags. - * @param timestamp a timestamp to format. - * @param timezone a TimeZone object. - * @return true if success, false if the tag could not be set. - */ - public boolean addDateTimeStampTag(int tagId, long timestamp, TimeZone timezone) { - if (tagId == TAG_DATE_TIME || tagId == TAG_DATE_TIME_DIGITIZED - || tagId == TAG_DATE_TIME_ORIGINAL) { - mDateTimeStampFormat.setTimeZone(timezone); - ExifTag t = buildTag(tagId, mDateTimeStampFormat.format(timestamp)); - if (t == null) { - return false; - } - setTag(t); - } else { - return false; - } - return true; - } - - /** - * Creates and sets all to the GPS tags for a give latitude and longitude. - * - * @param latitude a GPS latitude coordinate. - * @param longitude a GPS longitude coordinate. - * @return true if success, false if they could not be created or set. - */ - public boolean addGpsTags(double latitude, double longitude) { - ExifTag latTag = buildTag(TAG_GPS_LATITUDE, toExifLatLong(latitude)); - ExifTag longTag = buildTag(TAG_GPS_LONGITUDE, toExifLatLong(longitude)); - ExifTag latRefTag = buildTag(TAG_GPS_LATITUDE_REF, - latitude >= 0 ? GpsLatitudeRef.NORTH - : GpsLatitudeRef.SOUTH); - ExifTag longRefTag = buildTag(TAG_GPS_LONGITUDE_REF, - longitude >= 0 ? GpsLongitudeRef.EAST - : GpsLongitudeRef.WEST); - if (latTag == null || longTag == null || latRefTag == null || longRefTag == null) { - return false; - } - setTag(latTag); - setTag(longTag); - setTag(latRefTag); - setTag(longRefTag); - return true; - } - - /** - * Creates and sets the GPS timestamp tag. - * - * @param timestamp a GPS timestamp. - * @return true if success, false if could not be created or set. - */ - public boolean addGpsDateTimeStampTag(long timestamp) { - ExifTag t = buildTag(TAG_GPS_DATE_STAMP, mGPSDateStampFormat.format(timestamp)); - if (t == null) { - return false; - } - setTag(t); - mGPSTimeStampCalendar.setTimeInMillis(timestamp); - t = buildTag(TAG_GPS_TIME_STAMP, new Rational[] { - new Rational(mGPSTimeStampCalendar.get(Calendar.HOUR_OF_DAY), 1), - new Rational(mGPSTimeStampCalendar.get(Calendar.MINUTE), 1), - new Rational(mGPSTimeStampCalendar.get(Calendar.SECOND), 1) - }); - if (t == null) { - return false; - } - setTag(t); - return true; - } - - private static Rational[] toExifLatLong(double value) { - // convert to the format dd/1 mm/1 ssss/100 - value = Math.abs(value); - int degrees = (int) value; - value = (value - degrees) * 60; - int minutes = (int) value; - value = (value - minutes) * 6000; - int seconds = (int) value; - return new Rational[] { - new Rational(degrees, 1), new Rational(minutes, 1), new Rational(seconds, 100) - }; - } - - private void doExifStreamIO(InputStream is, OutputStream os) throws IOException { - byte[] buf = new byte[1024]; - int ret = is.read(buf, 0, 1024); - while (ret != -1) { - os.write(buf, 0, ret); - ret = is.read(buf, 0, 1024); - } - } - - protected static void closeSilently(Closeable c) { - if (c != null) { - try { - c.close(); - } catch (Throwable e) { - // ignored - } - } - } - - private SparseIntArray mTagInfo = null; - - protected SparseIntArray getTagInfo() { - if (mTagInfo == null) { - mTagInfo = new SparseIntArray(); - initTagInfo(); - } - return mTagInfo; - } - - private void initTagInfo() { - /** - * We put tag information in a 4-bytes integer. The first byte a bitmask - * representing the allowed IFDs of the tag, the second byte is the data - * type, and the last two byte are a short value indicating the default - * component count of this tag. - */ - // IFD0 tags - int[] ifdAllowedIfds = { - IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1 - }; - int ifdFlags = getFlagsFromAllowedIfds(ifdAllowedIfds) << 24; - mTagInfo.put(ExifInterface.TAG_MAKE, - ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_IMAGE_WIDTH, - ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); - mTagInfo.put(ExifInterface.TAG_IMAGE_LENGTH, - ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); - mTagInfo.put(ExifInterface.TAG_BITS_PER_SAMPLE, - ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 3); - mTagInfo.put(ExifInterface.TAG_COMPRESSION, - ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION, - ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_ORIENTATION, ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 - | 1); - mTagInfo.put(ExifInterface.TAG_SAMPLES_PER_PIXEL, - ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_PLANAR_CONFIGURATION, - ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING, - ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 2); - mTagInfo.put(ExifInterface.TAG_Y_CB_CR_POSITIONING, - ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_X_RESOLUTION, - ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_Y_RESOLUTION, - ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_RESOLUTION_UNIT, - ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_STRIP_OFFSETS, - ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_ROWS_PER_STRIP, - ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); - mTagInfo.put(ExifInterface.TAG_STRIP_BYTE_COUNTS, - ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_TRANSFER_FUNCTION, - ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 3 * 256); - mTagInfo.put(ExifInterface.TAG_WHITE_POINT, - ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 2); - mTagInfo.put(ExifInterface.TAG_PRIMARY_CHROMATICITIES, - ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 6); - mTagInfo.put(ExifInterface.TAG_Y_CB_CR_COEFFICIENTS, - ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 3); - mTagInfo.put(ExifInterface.TAG_REFERENCE_BLACK_WHITE, - ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 6); - mTagInfo.put(ExifInterface.TAG_DATE_TIME, - ifdFlags | ExifTag.TYPE_ASCII << 16 | 20); - mTagInfo.put(ExifInterface.TAG_IMAGE_DESCRIPTION, - ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_MAKE, - ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_MODEL, - ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_SOFTWARE, - ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_ARTIST, - ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_COPYRIGHT, - ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_EXIF_IFD, - ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); - mTagInfo.put(ExifInterface.TAG_GPS_IFD, - ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); - // IFD1 tags - int[] ifd1AllowedIfds = { - IfdId.TYPE_IFD_1 - }; - int ifdFlags1 = getFlagsFromAllowedIfds(ifd1AllowedIfds) << 24; - mTagInfo.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, - ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); - mTagInfo.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, - ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); - // Exif tags - int[] exifAllowedIfds = { - IfdId.TYPE_IFD_EXIF - }; - int exifFlags = getFlagsFromAllowedIfds(exifAllowedIfds) << 24; - mTagInfo.put(ExifInterface.TAG_EXIF_VERSION, - exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 4); - mTagInfo.put(ExifInterface.TAG_FLASHPIX_VERSION, - exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 4); - mTagInfo.put(ExifInterface.TAG_COLOR_SPACE, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_COMPONENTS_CONFIGURATION, - exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 4); - mTagInfo.put(ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL, - exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_PIXEL_X_DIMENSION, - exifFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); - mTagInfo.put(ExifInterface.TAG_PIXEL_Y_DIMENSION, - exifFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); - mTagInfo.put(ExifInterface.TAG_MAKER_NOTE, - exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_USER_COMMENT, - exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_RELATED_SOUND_FILE, - exifFlags | ExifTag.TYPE_ASCII << 16 | 13); - mTagInfo.put(ExifInterface.TAG_DATE_TIME_ORIGINAL, - exifFlags | ExifTag.TYPE_ASCII << 16 | 20); - mTagInfo.put(ExifInterface.TAG_DATE_TIME_DIGITIZED, - exifFlags | ExifTag.TYPE_ASCII << 16 | 20); - mTagInfo.put(ExifInterface.TAG_SUB_SEC_TIME, - exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_SUB_SEC_TIME_ORIGINAL, - exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_SUB_SEC_TIME_DIGITIZED, - exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_IMAGE_UNIQUE_ID, - exifFlags | ExifTag.TYPE_ASCII << 16 | 33); - mTagInfo.put(ExifInterface.TAG_EXPOSURE_TIME, - exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_F_NUMBER, - exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_EXPOSURE_PROGRAM, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_SPECTRAL_SENSITIVITY, - exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_ISO_SPEED_RATINGS, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_OECF, - exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_SHUTTER_SPEED_VALUE, - exifFlags | ExifTag.TYPE_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_APERTURE_VALUE, - exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_BRIGHTNESS_VALUE, - exifFlags | ExifTag.TYPE_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_EXPOSURE_BIAS_VALUE, - exifFlags | ExifTag.TYPE_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_MAX_APERTURE_VALUE, - exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_SUBJECT_DISTANCE, - exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_METERING_MODE, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_LIGHT_SOURCE, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_FLASH, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_FOCAL_LENGTH, - exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_SUBJECT_AREA, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_FLASH_ENERGY, - exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE, - exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION, - exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION, - exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_SUBJECT_LOCATION, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 2); - mTagInfo.put(ExifInterface.TAG_EXPOSURE_INDEX, - exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_SENSING_METHOD, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_FILE_SOURCE, - exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 1); - mTagInfo.put(ExifInterface.TAG_SCENE_TYPE, - exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 1); - mTagInfo.put(ExifInterface.TAG_CFA_PATTERN, - exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_CUSTOM_RENDERED, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_EXPOSURE_MODE, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_WHITE_BALANCE, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_DIGITAL_ZOOM_RATIO, - exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_FOCAL_LENGTH_IN_35_MM_FILE, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_SCENE_CAPTURE_TYPE, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_GAIN_CONTROL, - exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_CONTRAST, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_SATURATION, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_SHARPNESS, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION, - exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_SUBJECT_DISTANCE_RANGE, - exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); - mTagInfo.put(ExifInterface.TAG_INTEROPERABILITY_IFD, exifFlags - | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); - // GPS tag - int[] gpsAllowedIfds = { - IfdId.TYPE_IFD_GPS - }; - int gpsFlags = getFlagsFromAllowedIfds(gpsAllowedIfds) << 24; - mTagInfo.put(ExifInterface.TAG_GPS_VERSION_ID, - gpsFlags | ExifTag.TYPE_UNSIGNED_BYTE << 16 | 4); - mTagInfo.put(ExifInterface.TAG_GPS_LATITUDE_REF, - gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); - mTagInfo.put(ExifInterface.TAG_GPS_LONGITUDE_REF, - gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); - mTagInfo.put(ExifInterface.TAG_GPS_LATITUDE, - gpsFlags | ExifTag.TYPE_RATIONAL << 16 | 3); - mTagInfo.put(ExifInterface.TAG_GPS_LONGITUDE, - gpsFlags | ExifTag.TYPE_RATIONAL << 16 | 3); - mTagInfo.put(ExifInterface.TAG_GPS_ALTITUDE_REF, - gpsFlags | ExifTag.TYPE_UNSIGNED_BYTE << 16 | 1); - mTagInfo.put(ExifInterface.TAG_GPS_ALTITUDE, - gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_GPS_TIME_STAMP, - gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 3); - mTagInfo.put(ExifInterface.TAG_GPS_SATTELLITES, - gpsFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_GPS_STATUS, - gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); - mTagInfo.put(ExifInterface.TAG_GPS_MEASURE_MODE, - gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); - mTagInfo.put(ExifInterface.TAG_GPS_DOP, - gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_GPS_SPEED_REF, - gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); - mTagInfo.put(ExifInterface.TAG_GPS_SPEED, - gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_GPS_TRACK_REF, - gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); - mTagInfo.put(ExifInterface.TAG_GPS_TRACK, - gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_GPS_IMG_DIRECTION_REF, - gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); - mTagInfo.put(ExifInterface.TAG_GPS_IMG_DIRECTION, - gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_GPS_MAP_DATUM, - gpsFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_GPS_DEST_LATITUDE_REF, - gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); - mTagInfo.put(ExifInterface.TAG_GPS_DEST_LATITUDE, - gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_GPS_DEST_BEARING_REF, - gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); - mTagInfo.put(ExifInterface.TAG_GPS_DEST_BEARING, - gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_GPS_DEST_DISTANCE_REF, - gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); - mTagInfo.put(ExifInterface.TAG_GPS_DEST_DISTANCE, - gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); - mTagInfo.put(ExifInterface.TAG_GPS_PROCESSING_METHOD, - gpsFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_GPS_AREA_INFORMATION, - gpsFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); - mTagInfo.put(ExifInterface.TAG_GPS_DATE_STAMP, - gpsFlags | ExifTag.TYPE_ASCII << 16 | 11); - mTagInfo.put(ExifInterface.TAG_GPS_DIFFERENTIAL, - gpsFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 11); - // Interoperability tag - int[] interopAllowedIfds = { - IfdId.TYPE_IFD_INTEROPERABILITY - }; - int interopFlags = getFlagsFromAllowedIfds(interopAllowedIfds) << 24; - mTagInfo.put(TAG_INTEROPERABILITY_INDEX, interopFlags | ExifTag.TYPE_ASCII << 16 - | ExifTag.SIZE_UNDEFINED); - } - - protected static int getAllowedIfdFlagsFromInfo(int info) { - return info >>> 24; - } - - protected static int[] getAllowedIfdsFromInfo(int info) { - int ifdFlags = getAllowedIfdFlagsFromInfo(info); - int[] ifds = IfdData.getIfds(); - ArrayList<Integer> l = new ArrayList<Integer>(); - for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) { - int flag = (ifdFlags >> i) & 1; - if (flag == 1) { - l.add(ifds[i]); - } - } - if (l.size() <= 0) { - return null; - } - int[] ret = new int[l.size()]; - int j = 0; - for (int i : l) { - ret[j++] = i; - } - return ret; - } - - protected static boolean isIfdAllowed(int info, int ifd) { - int[] ifds = IfdData.getIfds(); - int ifdFlags = getAllowedIfdFlagsFromInfo(info); - for (int i = 0; i < ifds.length; i++) { - if (ifd == ifds[i] && ((ifdFlags >> i) & 1) == 1) { - return true; - } - } - return false; - } - - protected static int getFlagsFromAllowedIfds(int[] allowedIfds) { - if (allowedIfds == null || allowedIfds.length == 0) { - return 0; - } - int flags = 0; - int[] ifds = IfdData.getIfds(); - for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) { - for (int j : allowedIfds) { - if (ifds[i] == j) { - flags |= 1 << i; - break; - } - } - } - return flags; - } - - protected static short getTypeFromInfo(int info) { - return (short) ((info >> 16) & 0x0ff); - } - - protected static int getComponentCountFromInfo(int info) { - return info & 0x0ffff; - } - -} diff --git a/src/com/android/messaging/util/exif/ExifInvalidFormatException.java b/src/com/android/messaging/util/exif/ExifInvalidFormatException.java deleted file mode 100644 index a38f8a3..0000000 --- a/src/com/android/messaging/util/exif/ExifInvalidFormatException.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2012 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.android.messaging.util.exif; - -public class ExifInvalidFormatException extends Exception { - public ExifInvalidFormatException(String meg) { - super(meg); - } -} diff --git a/src/com/android/messaging/util/exif/ExifModifier.java b/src/com/android/messaging/util/exif/ExifModifier.java deleted file mode 100644 index 274022c..0000000 --- a/src/com/android/messaging/util/exif/ExifModifier.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (C) 2012 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.android.messaging.util.exif; - -import android.util.Log; -import com.android.messaging.util.LogUtil; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.ArrayList; -import java.util.List; - -class ExifModifier { - public static final String TAG = LogUtil.BUGLE_TAG; - public static final boolean DEBUG = false; - private final ByteBuffer mByteBuffer; - private final ExifData mTagToModified; - private final List<TagOffset> mTagOffsets = new ArrayList<TagOffset>(); - private final ExifInterface mInterface; - private int mOffsetBase; - - private static class TagOffset { - final int mOffset; - final ExifTag mTag; - - TagOffset(ExifTag tag, int offset) { - mTag = tag; - mOffset = offset; - } - } - - protected ExifModifier(ByteBuffer byteBuffer, ExifInterface iRef) throws IOException, - ExifInvalidFormatException { - mByteBuffer = byteBuffer; - mOffsetBase = byteBuffer.position(); - mInterface = iRef; - InputStream is = null; - try { - is = new ByteBufferInputStream(byteBuffer); - // Do not require any IFD; - ExifParser parser = ExifParser.parse(is, mInterface); - mTagToModified = new ExifData(parser.getByteOrder()); - mOffsetBase += parser.getTiffStartPosition(); - mByteBuffer.position(0); - } finally { - ExifInterface.closeSilently(is); - } - } - - protected ByteOrder getByteOrder() { - return mTagToModified.getByteOrder(); - } - - protected boolean commit() throws IOException, ExifInvalidFormatException { - InputStream is = null; - try { - is = new ByteBufferInputStream(mByteBuffer); - int flag = 0; - IfdData[] ifdDatas = new IfdData[] { - mTagToModified.getIfdData(IfdId.TYPE_IFD_0), - mTagToModified.getIfdData(IfdId.TYPE_IFD_1), - mTagToModified.getIfdData(IfdId.TYPE_IFD_EXIF), - mTagToModified.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY), - mTagToModified.getIfdData(IfdId.TYPE_IFD_GPS) - }; - - if (ifdDatas[IfdId.TYPE_IFD_0] != null) { - flag |= ExifParser.OPTION_IFD_0; - } - if (ifdDatas[IfdId.TYPE_IFD_1] != null) { - flag |= ExifParser.OPTION_IFD_1; - } - if (ifdDatas[IfdId.TYPE_IFD_EXIF] != null) { - flag |= ExifParser.OPTION_IFD_EXIF; - } - if (ifdDatas[IfdId.TYPE_IFD_GPS] != null) { - flag |= ExifParser.OPTION_IFD_GPS; - } - if (ifdDatas[IfdId.TYPE_IFD_INTEROPERABILITY] != null) { - flag |= ExifParser.OPTION_IFD_INTEROPERABILITY; - } - - ExifParser parser = ExifParser.parse(is, flag, mInterface); - int event = parser.next(); - IfdData currIfd = null; - while (event != ExifParser.EVENT_END) { - switch (event) { - case ExifParser.EVENT_START_OF_IFD: - currIfd = ifdDatas[parser.getCurrentIfd()]; - if (currIfd == null) { - parser.skipRemainingTagsInCurrentIfd(); - } - break; - case ExifParser.EVENT_NEW_TAG: - ExifTag oldTag = parser.getTag(); - ExifTag newTag = currIfd.getTag(oldTag.getTagId()); - if (newTag != null) { - if (newTag.getComponentCount() != oldTag.getComponentCount() - || newTag.getDataType() != oldTag.getDataType()) { - return false; - } else { - mTagOffsets.add(new TagOffset(newTag, oldTag.getOffset())); - currIfd.removeTag(oldTag.getTagId()); - if (currIfd.getTagCount() == 0) { - parser.skipRemainingTagsInCurrentIfd(); - } - } - } - break; - } - event = parser.next(); - } - for (IfdData ifd : ifdDatas) { - if (ifd != null && ifd.getTagCount() > 0) { - return false; - } - } - modify(); - } finally { - ExifInterface.closeSilently(is); - } - return true; - } - - private void modify() { - mByteBuffer.order(getByteOrder()); - for (TagOffset tagOffset : mTagOffsets) { - writeTagValue(tagOffset.mTag, tagOffset.mOffset); - } - } - - private void writeTagValue(ExifTag tag, int offset) { - if (DEBUG) { - Log.v(TAG, "modifying tag to: \n" + tag.toString()); - Log.v(TAG, "at offset: " + offset); - } - mByteBuffer.position(offset + mOffsetBase); - switch (tag.getDataType()) { - case ExifTag.TYPE_ASCII: - byte buf[] = tag.getStringByte(); - if (buf.length == tag.getComponentCount()) { - buf[buf.length - 1] = 0; - mByteBuffer.put(buf); - } else { - mByteBuffer.put(buf); - mByteBuffer.put((byte) 0); - } - break; - case ExifTag.TYPE_LONG: - case ExifTag.TYPE_UNSIGNED_LONG: - for (int i = 0, n = tag.getComponentCount(); i < n; i++) { - mByteBuffer.putInt((int) tag.getValueAt(i)); - } - break; - case ExifTag.TYPE_RATIONAL: - case ExifTag.TYPE_UNSIGNED_RATIONAL: - for (int i = 0, n = tag.getComponentCount(); i < n; i++) { - Rational v = tag.getRational(i); - mByteBuffer.putInt((int) v.getNumerator()); - mByteBuffer.putInt((int) v.getDenominator()); - } - break; - case ExifTag.TYPE_UNDEFINED: - case ExifTag.TYPE_UNSIGNED_BYTE: - buf = new byte[tag.getComponentCount()]; - tag.getBytes(buf); - mByteBuffer.put(buf); - break; - case ExifTag.TYPE_UNSIGNED_SHORT: - for (int i = 0, n = tag.getComponentCount(); i < n; i++) { - mByteBuffer.putShort((short) tag.getValueAt(i)); - } - break; - } - } - - public void modifyTag(ExifTag tag) { - mTagToModified.addTag(tag); - } -} diff --git a/src/com/android/messaging/util/exif/ExifOutputStream.java b/src/com/android/messaging/util/exif/ExifOutputStream.java deleted file mode 100644 index 2016da4..0000000 --- a/src/com/android/messaging/util/exif/ExifOutputStream.java +++ /dev/null @@ -1,522 +0,0 @@ -/* - * Copyright (C) 2012 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.android.messaging.util.exif; - -import android.util.Log; -import com.android.messaging.util.LogUtil; - -import java.io.BufferedOutputStream; -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.ArrayList; - -/** - * This class provides a way to replace the Exif header of a JPEG image. - * <p> - * Below is an example of writing EXIF data into a file - * - * <pre> - * public static void writeExif(byte[] jpeg, ExifData exif, String path) { - * OutputStream os = null; - * try { - * os = new FileOutputStream(path); - * ExifOutputStream eos = new ExifOutputStream(os); - * // Set the exif header - * eos.setExifData(exif); - * // Write the original jpeg out, the header will be add into the file. - * eos.write(jpeg); - * } catch (FileNotFoundException e) { - * e.printStackTrace(); - * } catch (IOException e) { - * e.printStackTrace(); - * } finally { - * if (os != null) { - * try { - * os.close(); - * } catch (IOException e) { - * e.printStackTrace(); - * } - * } - * } - * } - * </pre> - */ -class ExifOutputStream extends FilterOutputStream { - private static final String TAG = LogUtil.BUGLE_TAG; - private static final boolean DEBUG = false; - private static final int STREAMBUFFER_SIZE = 0x00010000; // 64Kb - - private static final int STATE_SOI = 0; - private static final int STATE_FRAME_HEADER = 1; - private static final int STATE_JPEG_DATA = 2; - - private static final int EXIF_HEADER = 0x45786966; - private static final short TIFF_HEADER = 0x002A; - private static final short TIFF_BIG_ENDIAN = 0x4d4d; - private static final short TIFF_LITTLE_ENDIAN = 0x4949; - private static final short TAG_SIZE = 12; - private static final short TIFF_HEADER_SIZE = 8; - private static final int MAX_EXIF_SIZE = 65535; - - private ExifData mExifData; - private int mState = STATE_SOI; - private int mByteToSkip; - private int mByteToCopy; - private final byte[] mSingleByteArray = new byte[1]; - private final ByteBuffer mBuffer = ByteBuffer.allocate(4); - private final ExifInterface mInterface; - - protected ExifOutputStream(OutputStream ou, ExifInterface iRef) { - super(new BufferedOutputStream(ou, STREAMBUFFER_SIZE)); - mInterface = iRef; - } - - /** - * Sets the ExifData to be written into the JPEG file. Should be called - * before writing image data. - */ - protected void setExifData(ExifData exifData) { - mExifData = exifData; - } - - /** - * Gets the Exif header to be written into the JPEF file. - */ - protected ExifData getExifData() { - return mExifData; - } - - private int requestByteToBuffer(int requestByteCount, byte[] buffer - , int offset, int length) { - int byteNeeded = requestByteCount - mBuffer.position(); - int byteToRead = length > byteNeeded ? byteNeeded : length; - mBuffer.put(buffer, offset, byteToRead); - return byteToRead; - } - - /** - * Writes the image out. The input data should be a valid JPEG format. After - * writing, it's Exif header will be replaced by the given header. - */ - @Override - public void write(byte[] buffer, int offset, int length) throws IOException { - while ((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA) - && length > 0) { - if (mByteToSkip > 0) { - int byteToProcess = length > mByteToSkip ? mByteToSkip : length; - length -= byteToProcess; - mByteToSkip -= byteToProcess; - offset += byteToProcess; - } - if (mByteToCopy > 0) { - int byteToProcess = length > mByteToCopy ? mByteToCopy : length; - out.write(buffer, offset, byteToProcess); - length -= byteToProcess; - mByteToCopy -= byteToProcess; - offset += byteToProcess; - } - if (length == 0) { - return; - } - switch (mState) { - case STATE_SOI: - int byteRead = requestByteToBuffer(2, buffer, offset, length); - offset += byteRead; - length -= byteRead; - if (mBuffer.position() < 2) { - return; - } - mBuffer.rewind(); - if (mBuffer.getShort() != JpegHeader.SOI) { - throw new IOException("Not a valid jpeg image, cannot write exif"); - } - out.write(mBuffer.array(), 0, 2); - mState = STATE_FRAME_HEADER; - mBuffer.rewind(); - writeExifData(); - break; - case STATE_FRAME_HEADER: - // We ignore the APP1 segment and copy all other segments - // until SOF tag. - byteRead = requestByteToBuffer(4, buffer, offset, length); - offset += byteRead; - length -= byteRead; - // Check if this image data doesn't contain SOF. - if (mBuffer.position() == 2) { - short tag = mBuffer.getShort(); - if (tag == JpegHeader.EOI) { - out.write(mBuffer.array(), 0, 2); - mBuffer.rewind(); - } - } - if (mBuffer.position() < 4) { - return; - } - mBuffer.rewind(); - short marker = mBuffer.getShort(); - if (marker == JpegHeader.APP1) { - mByteToSkip = (mBuffer.getShort() & 0x0000ffff) - 2; - mState = STATE_JPEG_DATA; - } else if (!JpegHeader.isSofMarker(marker)) { - out.write(mBuffer.array(), 0, 4); - mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2; - } else { - out.write(mBuffer.array(), 0, 4); - mState = STATE_JPEG_DATA; - } - mBuffer.rewind(); - } - } - if (length > 0) { - out.write(buffer, offset, length); - } - } - - /** - * Writes the one bytes out. The input data should be a valid JPEG format. - * After writing, it's Exif header will be replaced by the given header. - */ - @Override - public void write(int oneByte) throws IOException { - mSingleByteArray[0] = (byte) (0xff & oneByte); - write(mSingleByteArray); - } - - /** - * Equivalent to calling write(buffer, 0, buffer.length). - */ - @Override - public void write(byte[] buffer) throws IOException { - write(buffer, 0, buffer.length); - } - - private void writeExifData() throws IOException { - if (mExifData == null) { - return; - } - if (DEBUG) { - Log.v(TAG, "Writing exif data..."); - } - ArrayList<ExifTag> nullTags = stripNullValueTags(mExifData); - createRequiredIfdAndTag(); - int exifSize = calculateAllOffset(); - if (exifSize + 8 > MAX_EXIF_SIZE) { - throw new IOException("Exif header is too large (>64Kb)"); - } - OrderedDataOutputStream dataOutputStream = new OrderedDataOutputStream(out); - dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN); - dataOutputStream.writeShort(JpegHeader.APP1); - dataOutputStream.writeShort((short) (exifSize + 8)); - dataOutputStream.writeInt(EXIF_HEADER); - dataOutputStream.writeShort((short) 0x0000); - if (mExifData.getByteOrder() == ByteOrder.BIG_ENDIAN) { - dataOutputStream.writeShort(TIFF_BIG_ENDIAN); - } else { - dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN); - } - dataOutputStream.setByteOrder(mExifData.getByteOrder()); - dataOutputStream.writeShort(TIFF_HEADER); - dataOutputStream.writeInt(8); - writeAllTags(dataOutputStream); - writeThumbnail(dataOutputStream); - for (ExifTag t : nullTags) { - mExifData.addTag(t); - } - } - - private ArrayList<ExifTag> stripNullValueTags(ExifData data) { - ArrayList<ExifTag> nullTags = new ArrayList<ExifTag>(); - if (data.getAllTags() == null) { - return nullTags; - } - for (ExifTag t : data.getAllTags()) { - if (t.getValue() == null && !ExifInterface.isOffsetTag(t.getTagId())) { - data.removeTag(t.getTagId(), t.getIfd()); - nullTags.add(t); - } - } - return nullTags; - } - - private void writeThumbnail(OrderedDataOutputStream dataOutputStream) throws IOException { - if (mExifData.hasCompressedThumbnail()) { - dataOutputStream.write(mExifData.getCompressedThumbnail()); - } else if (mExifData.hasUncompressedStrip()) { - for (int i = 0; i < mExifData.getStripCount(); i++) { - dataOutputStream.write(mExifData.getStrip(i)); - } - } - } - - private void writeAllTags(OrderedDataOutputStream dataOutputStream) throws IOException { - writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_0), dataOutputStream); - writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_EXIF), dataOutputStream); - IfdData interoperabilityIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY); - if (interoperabilityIfd != null) { - writeIfd(interoperabilityIfd, dataOutputStream); - } - IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS); - if (gpsIfd != null) { - writeIfd(gpsIfd, dataOutputStream); - } - IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1); - if (ifd1 != null) { - writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_1), dataOutputStream); - } - } - - private void writeIfd(IfdData ifd, OrderedDataOutputStream dataOutputStream) - throws IOException { - ExifTag[] tags = ifd.getAllTags(); - dataOutputStream.writeShort((short) tags.length); - for (ExifTag tag : tags) { - dataOutputStream.writeShort(tag.getTagId()); - dataOutputStream.writeShort(tag.getDataType()); - dataOutputStream.writeInt(tag.getComponentCount()); - if (DEBUG) { - Log.v(TAG, "\n" + tag.toString()); - } - if (tag.getDataSize() > 4) { - dataOutputStream.writeInt(tag.getOffset()); - } else { - ExifOutputStream.writeTagValue(tag, dataOutputStream); - for (int i = 0, n = 4 - tag.getDataSize(); i < n; i++) { - dataOutputStream.write(0); - } - } - } - dataOutputStream.writeInt(ifd.getOffsetToNextIfd()); - for (ExifTag tag : tags) { - if (tag.getDataSize() > 4) { - ExifOutputStream.writeTagValue(tag, dataOutputStream); - } - } - } - - private int calculateOffsetOfIfd(IfdData ifd, int offset) { - offset += 2 + ifd.getTagCount() * TAG_SIZE + 4; - ExifTag[] tags = ifd.getAllTags(); - for (ExifTag tag : tags) { - if (tag.getDataSize() > 4) { - tag.setOffset(offset); - offset += tag.getDataSize(); - } - } - return offset; - } - - private void createRequiredIfdAndTag() throws IOException { - // IFD0 is required for all file - IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0); - if (ifd0 == null) { - ifd0 = new IfdData(IfdId.TYPE_IFD_0); - mExifData.addIfdData(ifd0); - } - ExifTag exifOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_EXIF_IFD); - if (exifOffsetTag == null) { - throw new IOException("No definition for crucial exif tag: " - + ExifInterface.TAG_EXIF_IFD); - } - ifd0.setTag(exifOffsetTag); - - // Exif IFD is required for all files. - IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF); - if (exifIfd == null) { - exifIfd = new IfdData(IfdId.TYPE_IFD_EXIF); - mExifData.addIfdData(exifIfd); - } - - // GPS IFD - IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS); - if (gpsIfd != null) { - ExifTag gpsOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_GPS_IFD); - if (gpsOffsetTag == null) { - throw new IOException("No definition for crucial exif tag: " - + ExifInterface.TAG_GPS_IFD); - } - ifd0.setTag(gpsOffsetTag); - } - - // Interoperability IFD - IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY); - if (interIfd != null) { - ExifTag interOffsetTag = mInterface - .buildUninitializedTag(ExifInterface.TAG_INTEROPERABILITY_IFD); - if (interOffsetTag == null) { - throw new IOException("No definition for crucial exif tag: " - + ExifInterface.TAG_INTEROPERABILITY_IFD); - } - exifIfd.setTag(interOffsetTag); - } - - IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1); - - // thumbnail - if (mExifData.hasCompressedThumbnail()) { - - if (ifd1 == null) { - ifd1 = new IfdData(IfdId.TYPE_IFD_1); - mExifData.addIfdData(ifd1); - } - - ExifTag offsetTag = mInterface - .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT); - if (offsetTag == null) { - throw new IOException("No definition for crucial exif tag: " - + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT); - } - - ifd1.setTag(offsetTag); - ExifTag lengthTag = mInterface - .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); - if (lengthTag == null) { - throw new IOException("No definition for crucial exif tag: " - + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); - } - - lengthTag.setValue(mExifData.getCompressedThumbnail().length); - ifd1.setTag(lengthTag); - - // Get rid of tags for uncompressed if they exist. - ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)); - ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS)); - } else if (mExifData.hasUncompressedStrip()) { - if (ifd1 == null) { - ifd1 = new IfdData(IfdId.TYPE_IFD_1); - mExifData.addIfdData(ifd1); - } - int stripCount = mExifData.getStripCount(); - ExifTag offsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_STRIP_OFFSETS); - if (offsetTag == null) { - throw new IOException("No definition for crucial exif tag: " - + ExifInterface.TAG_STRIP_OFFSETS); - } - ExifTag lengthTag = mInterface - .buildUninitializedTag(ExifInterface.TAG_STRIP_BYTE_COUNTS); - if (lengthTag == null) { - throw new IOException("No definition for crucial exif tag: " - + ExifInterface.TAG_STRIP_BYTE_COUNTS); - } - long[] lengths = new long[stripCount]; - for (int i = 0; i < mExifData.getStripCount(); i++) { - lengths[i] = mExifData.getStrip(i).length; - } - lengthTag.setValue(lengths); - ifd1.setTag(offsetTag); - ifd1.setTag(lengthTag); - // Get rid of tags for compressed if they exist. - ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)); - ifd1.removeTag(ExifInterface - .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)); - } else if (ifd1 != null) { - // Get rid of offset and length tags if there is no thumbnail. - ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)); - ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS)); - ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)); - ifd1.removeTag(ExifInterface - .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)); - } - } - - private int calculateAllOffset() { - int offset = TIFF_HEADER_SIZE; - IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0); - offset = calculateOffsetOfIfd(ifd0, offset); - ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD)).setValue(offset); - - IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF); - offset = calculateOffsetOfIfd(exifIfd, offset); - - IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY); - if (interIfd != null) { - exifIfd.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD)) - .setValue(offset); - offset = calculateOffsetOfIfd(interIfd, offset); - } - - IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS); - if (gpsIfd != null) { - ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD)).setValue(offset); - offset = calculateOffsetOfIfd(gpsIfd, offset); - } - - IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1); - if (ifd1 != null) { - ifd0.setOffsetToNextIfd(offset); - offset = calculateOffsetOfIfd(ifd1, offset); - } - - // thumbnail - if (mExifData.hasCompressedThumbnail()) { - ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)) - .setValue(offset); - offset += mExifData.getCompressedThumbnail().length; - } else if (mExifData.hasUncompressedStrip()) { - int stripCount = mExifData.getStripCount(); - long[] offsets = new long[stripCount]; - for (int i = 0; i < mExifData.getStripCount(); i++) { - offsets[i] = offset; - offset += mExifData.getStrip(i).length; - } - ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)).setValue( - offsets); - } - return offset; - } - - static void writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream) - throws IOException { - switch (tag.getDataType()) { - case ExifTag.TYPE_ASCII: - byte buf[] = tag.getStringByte(); - if (buf.length == tag.getComponentCount()) { - buf[buf.length - 1] = 0; - dataOutputStream.write(buf); - } else { - dataOutputStream.write(buf); - dataOutputStream.write(0); - } - break; - case ExifTag.TYPE_LONG: - case ExifTag.TYPE_UNSIGNED_LONG: - for (int i = 0, n = tag.getComponentCount(); i < n; i++) { - dataOutputStream.writeInt((int) tag.getValueAt(i)); - } - break; - case ExifTag.TYPE_RATIONAL: - case ExifTag.TYPE_UNSIGNED_RATIONAL: - for (int i = 0, n = tag.getComponentCount(); i < n; i++) { - dataOutputStream.writeRational(tag.getRational(i)); - } - break; - case ExifTag.TYPE_UNDEFINED: - case ExifTag.TYPE_UNSIGNED_BYTE: - buf = new byte[tag.getComponentCount()]; - tag.getBytes(buf); - dataOutputStream.write(buf); - break; - case ExifTag.TYPE_UNSIGNED_SHORT: - for (int i = 0, n = tag.getComponentCount(); i < n; i++) { - dataOutputStream.writeShort((short) tag.getValueAt(i)); - } - break; - } - } -} diff --git a/src/com/android/messaging/util/exif/ExifParser.java b/src/com/android/messaging/util/exif/ExifParser.java deleted file mode 100644 index 4b6cf68..0000000 --- a/src/com/android/messaging/util/exif/ExifParser.java +++ /dev/null @@ -1,918 +0,0 @@ -/* - * Copyright (C) 2012 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.android.messaging.util.exif; - -import android.util.Log; -import com.android.messaging.util.LogUtil; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteOrder; -import java.nio.charset.Charset; -import java.util.Map.Entry; -import java.util.TreeMap; - -/** - * This class provides a low-level EXIF parsing API. Given a JPEG format - * InputStream, the caller can request which IFD's to read via - * {@link #parse(java.io.InputStream, int)} with given options. - * <p> - * Below is an example of getting EXIF data from IFD 0 and EXIF IFD using the - * parser. - * - * <pre> - * void parse() { - * ExifParser parser = ExifParser.parse(mImageInputStream, - * ExifParser.OPTION_IFD_0 | ExifParser.OPTIONS_IFD_EXIF); - * int event = parser.next(); - * while (event != ExifParser.EVENT_END) { - * switch (event) { - * case ExifParser.EVENT_START_OF_IFD: - * break; - * case ExifParser.EVENT_NEW_TAG: - * ExifTag tag = parser.getTag(); - * if (!tag.hasValue()) { - * parser.registerForTagValue(tag); - * } else { - * processTag(tag); - * } - * break; - * case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG: - * tag = parser.getTag(); - * if (tag.getDataType() != ExifTag.TYPE_UNDEFINED) { - * processTag(tag); - * } - * break; - * } - * event = parser.next(); - * } - * } - * - * void processTag(ExifTag tag) { - * // process the tag as you like. - * } - * </pre> - */ -public class ExifParser { - private static final boolean LOGV = false; - private static final String TAG = LogUtil.BUGLE_TAG; - /** - * When the parser reaches a new IFD area. Call {@link #getCurrentIfd()} to - * know which IFD we are in. - */ - public static final int EVENT_START_OF_IFD = 0; - /** - * When the parser reaches a new tag. Call {@link #getTag()}to get the - * corresponding tag. - */ - public static final int EVENT_NEW_TAG = 1; - /** - * When the parser reaches the value area of tag that is registered by - * {@link #registerForTagValue(ExifTag)} previously. Call {@link #getTag()} - * to get the corresponding tag. - */ - public static final int EVENT_VALUE_OF_REGISTERED_TAG = 2; - - /** - * When the parser reaches the compressed image area. - */ - public static final int EVENT_COMPRESSED_IMAGE = 3; - /** - * When the parser reaches the uncompressed image strip. Call - * {@link #getStripIndex()} to get the index of the strip. - * - * @see #getStripIndex() - * @see #getStripCount() - */ - public static final int EVENT_UNCOMPRESSED_STRIP = 4; - /** - * When there is nothing more to parse. - */ - public static final int EVENT_END = 5; - - /** - * Option bit to request to parse IFD0. - */ - public static final int OPTION_IFD_0 = 1 << 0; - /** - * Option bit to request to parse IFD1. - */ - public static final int OPTION_IFD_1 = 1 << 1; - /** - * Option bit to request to parse Exif-IFD. - */ - public static final int OPTION_IFD_EXIF = 1 << 2; - /** - * Option bit to request to parse GPS-IFD. - */ - public static final int OPTION_IFD_GPS = 1 << 3; - /** - * Option bit to request to parse Interoperability-IFD. - */ - public static final int OPTION_IFD_INTEROPERABILITY = 1 << 4; - /** - * Option bit to request to parse thumbnail. - */ - public static final int OPTION_THUMBNAIL = 1 << 5; - - protected static final int EXIF_HEADER = 0x45786966; // EXIF header "Exif" - protected static final short EXIF_HEADER_TAIL = (short) 0x0000; // EXIF header in APP1 - - // TIFF header - protected static final short LITTLE_ENDIAN_TAG = (short) 0x4949; // "II" - protected static final short BIG_ENDIAN_TAG = (short) 0x4d4d; // "MM" - protected static final short TIFF_HEADER_TAIL = 0x002A; - - protected static final int TAG_SIZE = 12; - protected static final int OFFSET_SIZE = 2; - - private static final Charset US_ASCII = Charset.forName("US-ASCII"); - - protected static final int DEFAULT_IFD0_OFFSET = 8; - - private final CountedDataInputStream mTiffStream; - private final int mOptions; - private int mIfdStartOffset = 0; - private int mNumOfTagInIfd = 0; - private int mIfdType; - private ExifTag mTag; - private ImageEvent mImageEvent; - private int mStripCount; - private ExifTag mStripSizeTag; - private ExifTag mJpegSizeTag; - private boolean mNeedToParseOffsetsInCurrentIfd; - private boolean mContainExifData = false; - private int mApp1End; - private int mOffsetToApp1EndFromSOF = 0; - private byte[] mDataAboveIfd0; - private int mIfd0Position; - private int mTiffStartPosition; - private final ExifInterface mInterface; - - private static final short TAG_EXIF_IFD = ExifInterface - .getTrueTagKey(ExifInterface.TAG_EXIF_IFD); - private static final short TAG_GPS_IFD = ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD); - private static final short TAG_INTEROPERABILITY_IFD = ExifInterface - .getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD); - private static final short TAG_JPEG_INTERCHANGE_FORMAT = ExifInterface - .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT); - private static final short TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = ExifInterface - .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); - private static final short TAG_STRIP_OFFSETS = ExifInterface - .getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS); - private static final short TAG_STRIP_BYTE_COUNTS = ExifInterface - .getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS); - - private final TreeMap<Integer, Object> mCorrespondingEvent = new TreeMap<Integer, Object>(); - - private boolean isIfdRequested(int ifdType) { - switch (ifdType) { - case IfdId.TYPE_IFD_0: - return (mOptions & OPTION_IFD_0) != 0; - case IfdId.TYPE_IFD_1: - return (mOptions & OPTION_IFD_1) != 0; - case IfdId.TYPE_IFD_EXIF: - return (mOptions & OPTION_IFD_EXIF) != 0; - case IfdId.TYPE_IFD_GPS: - return (mOptions & OPTION_IFD_GPS) != 0; - case IfdId.TYPE_IFD_INTEROPERABILITY: - return (mOptions & OPTION_IFD_INTEROPERABILITY) != 0; - } - return false; - } - - private boolean isThumbnailRequested() { - return (mOptions & OPTION_THUMBNAIL) != 0; - } - - private ExifParser(InputStream inputStream, int options, ExifInterface iRef) - throws IOException, ExifInvalidFormatException { - if (inputStream == null) { - throw new IOException("Null argument inputStream to ExifParser"); - } - if (LOGV) { - Log.v(TAG, "Reading exif..."); - } - mInterface = iRef; - mContainExifData = seekTiffData(inputStream); - mTiffStream = new CountedDataInputStream(inputStream); - mOptions = options; - if (!mContainExifData) { - return; - } - - parseTiffHeader(); - long offset = mTiffStream.readUnsignedInt(); - if (offset > Integer.MAX_VALUE) { - throw new ExifInvalidFormatException("Invalid offset " + offset); - } - mIfd0Position = (int) offset; - mIfdType = IfdId.TYPE_IFD_0; - if (isIfdRequested(IfdId.TYPE_IFD_0) || needToParseOffsetsInCurrentIfd()) { - registerIfd(IfdId.TYPE_IFD_0, offset); - if (offset != DEFAULT_IFD0_OFFSET) { - mDataAboveIfd0 = new byte[(int) offset - DEFAULT_IFD0_OFFSET]; - read(mDataAboveIfd0); - } - } - } - - /** - * Parses the the given InputStream with the given options - * - * @exception java.io.IOException - * @exception ExifInvalidFormatException - */ - protected static ExifParser parse(InputStream inputStream, int options, ExifInterface iRef) - throws IOException, ExifInvalidFormatException { - return new ExifParser(inputStream, options, iRef); - } - - /** - * Parses the the given InputStream with default options; that is, every IFD - * and thumbnaill will be parsed. - * - * @exception java.io.IOException - * @exception ExifInvalidFormatException - * @see #parse(java.io.InputStream, int) - */ - protected static ExifParser parse(InputStream inputStream, ExifInterface iRef) - throws IOException, ExifInvalidFormatException { - return new ExifParser(inputStream, OPTION_IFD_0 | OPTION_IFD_1 - | OPTION_IFD_EXIF | OPTION_IFD_GPS | OPTION_IFD_INTEROPERABILITY - | OPTION_THUMBNAIL, iRef); - } - - /** - * Moves the parser forward and returns the next parsing event - * - * @exception java.io.IOException - * @exception ExifInvalidFormatException - * @see #EVENT_START_OF_IFD - * @see #EVENT_NEW_TAG - * @see #EVENT_VALUE_OF_REGISTERED_TAG - * @see #EVENT_COMPRESSED_IMAGE - * @see #EVENT_UNCOMPRESSED_STRIP - * @see #EVENT_END - */ - protected int next() throws IOException, ExifInvalidFormatException { - if (!mContainExifData) { - return EVENT_END; - } - int offset = mTiffStream.getReadByteCount(); - int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd; - if (offset < endOfTags) { - mTag = readTag(); - if (mTag == null) { - return next(); - } - if (mNeedToParseOffsetsInCurrentIfd) { - checkOffsetOrImageTag(mTag); - } - return EVENT_NEW_TAG; - } else if (offset == endOfTags) { - // There is a link to ifd1 at the end of ifd0 - if (mIfdType == IfdId.TYPE_IFD_0) { - long ifdOffset = readUnsignedLong(); - if (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested()) { - if (ifdOffset != 0) { - registerIfd(IfdId.TYPE_IFD_1, ifdOffset); - } - } - } else { - int offsetSize = 4; - // Some camera models use invalid length of the offset - if (mCorrespondingEvent.size() > 0) { - offsetSize = mCorrespondingEvent.firstEntry().getKey() - - mTiffStream.getReadByteCount(); - } - if (offsetSize < 4) { - Log.w(TAG, "Invalid size of link to next IFD: " + offsetSize); - } else { - long ifdOffset = readUnsignedLong(); - if (ifdOffset != 0) { - Log.w(TAG, "Invalid link to next IFD: " + ifdOffset); - } - } - } - } - while (mCorrespondingEvent.size() != 0) { - Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry(); - Object event = entry.getValue(); - try { - skipTo(entry.getKey()); - } catch (IOException e) { - Log.w(TAG, "Failed to skip to data at: " + entry.getKey() + - " for " + event.getClass().getName() + ", the file may be broken."); - continue; - } - if (event instanceof IfdEvent) { - mIfdType = ((IfdEvent) event).ifd; - mNumOfTagInIfd = mTiffStream.readUnsignedShort(); - mIfdStartOffset = entry.getKey(); - - if (mNumOfTagInIfd * TAG_SIZE + mIfdStartOffset + OFFSET_SIZE > mApp1End) { - Log.w(TAG, "Invalid size of IFD " + mIfdType); - return EVENT_END; - } - - mNeedToParseOffsetsInCurrentIfd = needToParseOffsetsInCurrentIfd(); - if (((IfdEvent) event).isRequested) { - return EVENT_START_OF_IFD; - } else { - skipRemainingTagsInCurrentIfd(); - } - } else if (event instanceof ImageEvent) { - mImageEvent = (ImageEvent) event; - return mImageEvent.type; - } else { - ExifTagEvent tagEvent = (ExifTagEvent) event; - mTag = tagEvent.tag; - if (mTag.getDataType() != ExifTag.TYPE_UNDEFINED) { - readFullTagValue(mTag); - checkOffsetOrImageTag(mTag); - } - if (tagEvent.isRequested) { - return EVENT_VALUE_OF_REGISTERED_TAG; - } - } - } - return EVENT_END; - } - - /** - * Skips the tags area of current IFD, if the parser is not in the tag area, - * nothing will happen. - * - * @throws java.io.IOException - * @throws ExifInvalidFormatException - */ - protected void skipRemainingTagsInCurrentIfd() throws IOException, ExifInvalidFormatException { - int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd; - int offset = mTiffStream.getReadByteCount(); - if (offset > endOfTags) { - return; - } - if (mNeedToParseOffsetsInCurrentIfd) { - while (offset < endOfTags) { - mTag = readTag(); - offset += TAG_SIZE; - if (mTag == null) { - continue; - } - checkOffsetOrImageTag(mTag); - } - } else { - skipTo(endOfTags); - } - long ifdOffset = readUnsignedLong(); - // For ifd0, there is a link to ifd1 in the end of all tags - if (mIfdType == IfdId.TYPE_IFD_0 - && (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested())) { - if (ifdOffset > 0) { - registerIfd(IfdId.TYPE_IFD_1, ifdOffset); - } - } - } - - private boolean needToParseOffsetsInCurrentIfd() { - switch (mIfdType) { - case IfdId.TYPE_IFD_0: - return isIfdRequested(IfdId.TYPE_IFD_EXIF) || isIfdRequested(IfdId.TYPE_IFD_GPS) - || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY) - || isIfdRequested(IfdId.TYPE_IFD_1); - case IfdId.TYPE_IFD_1: - return isThumbnailRequested(); - case IfdId.TYPE_IFD_EXIF: - // The offset to interoperability IFD is located in Exif IFD - return isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY); - default: - return false; - } - } - - /** - * If {@link #next()} return {@link #EVENT_NEW_TAG} or - * {@link #EVENT_VALUE_OF_REGISTERED_TAG}, call this function to get the - * corresponding tag. - * <p> - * For {@link #EVENT_NEW_TAG}, the tag may not contain the value if the size - * of the value is greater than 4 bytes. One should call - * {@link ExifTag#hasValue()} to check if the tag contains value. If there - * is no value,call {@link #registerForTagValue(ExifTag)} to have the parser - * emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area - * pointed by the offset. - * <p> - * When {@link #EVENT_VALUE_OF_REGISTERED_TAG} is emitted, the value of the - * tag will have already been read except for tags of undefined type. For - * tags of undefined type, call one of the read methods to get the value. - * - * @see #registerForTagValue(ExifTag) - * @see #read(byte[]) - * @see #read(byte[], int, int) - * @see #readLong() - * @see #readRational() - * @see #readString(int) - * @see #readString(int, java.nio.charset.Charset) - */ - protected ExifTag getTag() { - return mTag; - } - - /** - * Gets number of tags in the current IFD area. - */ - protected int getTagCountInCurrentIfd() { - return mNumOfTagInIfd; - } - - /** - * Gets the ID of current IFD. - * - * @see IfdId#TYPE_IFD_0 - * @see IfdId#TYPE_IFD_1 - * @see IfdId#TYPE_IFD_GPS - * @see IfdId#TYPE_IFD_INTEROPERABILITY - * @see IfdId#TYPE_IFD_EXIF - */ - protected int getCurrentIfd() { - return mIfdType; - } - - /** - * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to - * get the index of this strip. - * - * @see #getStripCount() - */ - protected int getStripIndex() { - return mImageEvent.stripIndex; - } - - /** - * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to - * get the number of strip data. - * - * @see #getStripIndex() - */ - protected int getStripCount() { - return mStripCount; - } - - /** - * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to - * get the strip size. - */ - protected int getStripSize() { - if (mStripSizeTag == null) { - return 0; - } - return (int) mStripSizeTag.getValueAt(0); - } - - /** - * When receiving {@link #EVENT_COMPRESSED_IMAGE}, call this function to get - * the image data size. - */ - protected int getCompressedImageSize() { - if (mJpegSizeTag == null) { - return 0; - } - return (int) mJpegSizeTag.getValueAt(0); - } - - private void skipTo(int offset) throws IOException { - mTiffStream.skipTo(offset); - while (!mCorrespondingEvent.isEmpty() && mCorrespondingEvent.firstKey() < offset) { - mCorrespondingEvent.pollFirstEntry(); - } - } - - /** - * When getting {@link #EVENT_NEW_TAG} in the tag area of IFD, the tag may - * not contain the value if the size of the value is greater than 4 bytes. - * When the value is not available here, call this method so that the parser - * will emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area - * where the value is located. - * - * @see #EVENT_VALUE_OF_REGISTERED_TAG - */ - protected void registerForTagValue(ExifTag tag) { - if (tag.getOffset() >= mTiffStream.getReadByteCount()) { - mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, true)); - } - } - - private void registerIfd(int ifdType, long offset) { - // Cast unsigned int to int since the offset is always smaller - // than the size of APP1 (65536) - mCorrespondingEvent.put((int) offset, new IfdEvent(ifdType, isIfdRequested(ifdType))); - } - - private void registerCompressedImage(long offset) { - mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_COMPRESSED_IMAGE)); - } - - private void registerUncompressedStrip(int stripIndex, long offset) { - mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_UNCOMPRESSED_STRIP - , stripIndex)); - } - - private ExifTag readTag() throws IOException, ExifInvalidFormatException { - short tagId = mTiffStream.readShort(); - short dataFormat = mTiffStream.readShort(); - long numOfComp = mTiffStream.readUnsignedInt(); - if (numOfComp > Integer.MAX_VALUE) { - throw new ExifInvalidFormatException( - "Number of component is larger then Integer.MAX_VALUE"); - } - // Some invalid image file contains invalid data type. Ignore those tags - if (!ExifTag.isValidType(dataFormat)) { - Log.w(TAG, String.format("Tag %04x: Invalid data type %d", tagId, dataFormat)); - mTiffStream.skip(4); - return null; - } - // TODO: handle numOfComp overflow - ExifTag tag = new ExifTag(tagId, dataFormat, (int) numOfComp, mIfdType, - ((int) numOfComp) != ExifTag.SIZE_UNDEFINED); - int dataSize = tag.getDataSize(); - if (dataSize > 4) { - long offset = mTiffStream.readUnsignedInt(); - if (offset > Integer.MAX_VALUE) { - throw new ExifInvalidFormatException( - "offset is larger then Integer.MAX_VALUE"); - } - // Some invalid images put some undefined data before IFD0. - // Read the data here. - if ((offset < mIfd0Position) && (dataFormat == ExifTag.TYPE_UNDEFINED)) { - byte[] buf = new byte[(int) numOfComp]; - System.arraycopy(mDataAboveIfd0, (int) offset - DEFAULT_IFD0_OFFSET, - buf, 0, (int) numOfComp); - tag.setValue(buf); - } else { - tag.setOffset((int) offset); - } - } else { - boolean defCount = tag.hasDefinedCount(); - // Set defined count to 0 so we can add \0 to non-terminated strings - tag.setHasDefinedCount(false); - // Read value - readFullTagValue(tag); - tag.setHasDefinedCount(defCount); - mTiffStream.skip(4 - dataSize); - // Set the offset to the position of value. - tag.setOffset(mTiffStream.getReadByteCount() - 4); - } - return tag; - } - - /** - * Check the tag, if the tag is one of the offset tag that points to the IFD - * or image the caller is interested in, register the IFD or image. - */ - private void checkOffsetOrImageTag(ExifTag tag) { - // Some invalid formattd image contains tag with 0 size. - if (tag.getComponentCount() == 0) { - return; - } - short tid = tag.getTagId(); - int ifd = tag.getIfd(); - if (tid == TAG_EXIF_IFD && checkAllowed(ifd, ExifInterface.TAG_EXIF_IFD)) { - if (isIfdRequested(IfdId.TYPE_IFD_EXIF) - || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) { - registerIfd(IfdId.TYPE_IFD_EXIF, tag.getValueAt(0)); - } - } else if (tid == TAG_GPS_IFD && checkAllowed(ifd, ExifInterface.TAG_GPS_IFD)) { - if (isIfdRequested(IfdId.TYPE_IFD_GPS)) { - registerIfd(IfdId.TYPE_IFD_GPS, tag.getValueAt(0)); - } - } else if (tid == TAG_INTEROPERABILITY_IFD - && checkAllowed(ifd, ExifInterface.TAG_INTEROPERABILITY_IFD)) { - if (isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) { - registerIfd(IfdId.TYPE_IFD_INTEROPERABILITY, tag.getValueAt(0)); - } - } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT - && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)) { - if (isThumbnailRequested()) { - registerCompressedImage(tag.getValueAt(0)); - } - } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT_LENGTH - && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)) { - if (isThumbnailRequested()) { - mJpegSizeTag = tag; - } - } else if (tid == TAG_STRIP_OFFSETS && checkAllowed(ifd, ExifInterface.TAG_STRIP_OFFSETS)) { - if (isThumbnailRequested()) { - if (tag.hasValue()) { - for (int i = 0; i < tag.getComponentCount(); i++) { - if (tag.getDataType() == ExifTag.TYPE_UNSIGNED_SHORT) { - registerUncompressedStrip(i, tag.getValueAt(i)); - } else { - registerUncompressedStrip(i, tag.getValueAt(i)); - } - } - } else { - mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, false)); - } - } - } else if (tid == TAG_STRIP_BYTE_COUNTS - && checkAllowed(ifd, ExifInterface.TAG_STRIP_BYTE_COUNTS) - && isThumbnailRequested() && tag.hasValue()) { - mStripSizeTag = tag; - } - } - - private boolean checkAllowed(int ifd, int tagId) { - int info = mInterface.getTagInfo().get(tagId); - if (info == ExifInterface.DEFINITION_NULL) { - return false; - } - return ExifInterface.isIfdAllowed(info, ifd); - } - - protected void readFullTagValue(ExifTag tag) throws IOException { - // Some invalid images contains tags with wrong size, check it here - short type = tag.getDataType(); - if (type == ExifTag.TYPE_ASCII || type == ExifTag.TYPE_UNDEFINED || - type == ExifTag.TYPE_UNSIGNED_BYTE) { - int size = tag.getComponentCount(); - if (mCorrespondingEvent.size() > 0) { - if (mCorrespondingEvent.firstEntry().getKey() < mTiffStream.getReadByteCount() - + size) { - Object event = mCorrespondingEvent.firstEntry().getValue(); - if (event instanceof ImageEvent) { - // Tag value overlaps thumbnail, ignore thumbnail. - Log.w(TAG, "Thumbnail overlaps value for tag: \n" + tag.toString()); - Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry(); - Log.w(TAG, "Invalid thumbnail offset: " + entry.getKey()); - } else { - // Tag value overlaps another tag, shorten count - if (event instanceof IfdEvent) { - Log.w(TAG, "Ifd " + ((IfdEvent) event).ifd - + " overlaps value for tag: \n" + tag.toString()); - } else if (event instanceof ExifTagEvent) { - Log.w(TAG, "Tag value for tag: \n" - + ((ExifTagEvent) event).tag.toString() - + " overlaps value for tag: \n" + tag.toString()); - } - size = mCorrespondingEvent.firstEntry().getKey() - - mTiffStream.getReadByteCount(); - Log.w(TAG, "Invalid size of tag: \n" + tag.toString() - + " setting count to: " + size); - tag.forceSetComponentCount(size); - } - } - } - } - switch (tag.getDataType()) { - case ExifTag.TYPE_UNSIGNED_BYTE: - case ExifTag.TYPE_UNDEFINED: { - byte buf[] = new byte[tag.getComponentCount()]; - read(buf); - tag.setValue(buf); - } - break; - case ExifTag.TYPE_ASCII: - tag.setValue(readString(tag.getComponentCount())); - break; - case ExifTag.TYPE_UNSIGNED_LONG: { - long value[] = new long[tag.getComponentCount()]; - for (int i = 0, n = value.length; i < n; i++) { - value[i] = readUnsignedLong(); - } - tag.setValue(value); - } - break; - case ExifTag.TYPE_UNSIGNED_RATIONAL: { - Rational value[] = new Rational[tag.getComponentCount()]; - for (int i = 0, n = value.length; i < n; i++) { - value[i] = readUnsignedRational(); - } - tag.setValue(value); - } - break; - case ExifTag.TYPE_UNSIGNED_SHORT: { - int value[] = new int[tag.getComponentCount()]; - for (int i = 0, n = value.length; i < n; i++) { - value[i] = readUnsignedShort(); - } - tag.setValue(value); - } - break; - case ExifTag.TYPE_LONG: { - int value[] = new int[tag.getComponentCount()]; - for (int i = 0, n = value.length; i < n; i++) { - value[i] = readLong(); - } - tag.setValue(value); - } - break; - case ExifTag.TYPE_RATIONAL: { - Rational value[] = new Rational[tag.getComponentCount()]; - for (int i = 0, n = value.length; i < n; i++) { - value[i] = readRational(); - } - tag.setValue(value); - } - break; - } - if (LOGV) { - Log.v(TAG, "\n" + tag.toString()); - } - } - - private void parseTiffHeader() throws IOException, - ExifInvalidFormatException { - short byteOrder = mTiffStream.readShort(); - if (LITTLE_ENDIAN_TAG == byteOrder) { - mTiffStream.setByteOrder(ByteOrder.LITTLE_ENDIAN); - } else if (BIG_ENDIAN_TAG == byteOrder) { - mTiffStream.setByteOrder(ByteOrder.BIG_ENDIAN); - } else { - throw new ExifInvalidFormatException("Invalid TIFF header"); - } - - if (mTiffStream.readShort() != TIFF_HEADER_TAIL) { - throw new ExifInvalidFormatException("Invalid TIFF header"); - } - } - - private boolean seekTiffData(InputStream inputStream) throws IOException, - ExifInvalidFormatException { - CountedDataInputStream dataStream = new CountedDataInputStream(inputStream); - if (dataStream.readShort() != JpegHeader.SOI) { - throw new ExifInvalidFormatException("Invalid JPEG format"); - } - - short marker = dataStream.readShort(); - while (marker != JpegHeader.EOI - && !JpegHeader.isSofMarker(marker)) { - int length = dataStream.readUnsignedShort(); - // Some invalid formatted image contains multiple APP1, - // try to find the one with Exif data. - if (marker == JpegHeader.APP1) { - int header = 0; - short headerTail = 0; - if (length >= 8) { - header = dataStream.readInt(); - headerTail = dataStream.readShort(); - length -= 6; - if (header == EXIF_HEADER && headerTail == EXIF_HEADER_TAIL) { - mTiffStartPosition = dataStream.getReadByteCount(); - mApp1End = length; - mOffsetToApp1EndFromSOF = mTiffStartPosition + mApp1End; - return true; - } - } - } - if (length < 2 || (length - 2) != dataStream.skip(length - 2)) { - Log.w(TAG, "Invalid JPEG format."); - return false; - } - marker = dataStream.readShort(); - } - return false; - } - - protected int getOffsetToExifEndFromSOF() { - return mOffsetToApp1EndFromSOF; - } - - protected int getTiffStartPosition() { - return mTiffStartPosition; - } - - /** - * Reads bytes from the InputStream. - */ - protected int read(byte[] buffer, int offset, int length) throws IOException { - return mTiffStream.read(buffer, offset, length); - } - - /** - * Equivalent to read(buffer, 0, buffer.length). - */ - protected int read(byte[] buffer) throws IOException { - return mTiffStream.read(buffer); - } - - /** - * Reads a String from the InputStream with US-ASCII charset. The parser - * will read n bytes and convert it to ascii string. This is used for - * reading values of type {@link ExifTag#TYPE_ASCII}. - */ - protected String readString(int n) throws IOException { - return readString(n, US_ASCII); - } - - /** - * Reads a String from the InputStream with the given charset. The parser - * will read n bytes and convert it to string. This is used for reading - * values of type {@link ExifTag#TYPE_ASCII}. - */ - protected String readString(int n, Charset charset) throws IOException { - if (n > 0) { - return mTiffStream.readString(n, charset); - } else { - return ""; - } - } - - /** - * Reads value of type {@link ExifTag#TYPE_UNSIGNED_SHORT} from the - * InputStream. - */ - protected int readUnsignedShort() throws IOException { - return mTiffStream.readShort() & 0xffff; - } - - /** - * Reads value of type {@link ExifTag#TYPE_UNSIGNED_LONG} from the - * InputStream. - */ - protected long readUnsignedLong() throws IOException { - return readLong() & 0xffffffffL; - } - - /** - * Reads value of type {@link ExifTag#TYPE_UNSIGNED_RATIONAL} from the - * InputStream. - */ - protected Rational readUnsignedRational() throws IOException { - long nomi = readUnsignedLong(); - long denomi = readUnsignedLong(); - return new Rational(nomi, denomi); - } - - /** - * Reads value of type {@link ExifTag#TYPE_LONG} from the InputStream. - */ - protected int readLong() throws IOException { - return mTiffStream.readInt(); - } - - /** - * Reads value of type {@link ExifTag#TYPE_RATIONAL} from the InputStream. - */ - protected Rational readRational() throws IOException { - int nomi = readLong(); - int denomi = readLong(); - return new Rational(nomi, denomi); - } - - private static class ImageEvent { - int stripIndex; - int type; - - ImageEvent(int type) { - this.stripIndex = 0; - this.type = type; - } - - ImageEvent(int type, int stripIndex) { - this.type = type; - this.stripIndex = stripIndex; - } - } - - private static class IfdEvent { - int ifd; - boolean isRequested; - - IfdEvent(int ifd, boolean isInterestedIfd) { - this.ifd = ifd; - this.isRequested = isInterestedIfd; - } - } - - private static class ExifTagEvent { - ExifTag tag; - boolean isRequested; - - ExifTagEvent(ExifTag tag, boolean isRequireByUser) { - this.tag = tag; - this.isRequested = isRequireByUser; - } - } - - /** - * Gets the byte order of the current InputStream. - */ - protected ByteOrder getByteOrder() { - return mTiffStream.getByteOrder(); - } -} diff --git a/src/com/android/messaging/util/exif/ExifReader.java b/src/com/android/messaging/util/exif/ExifReader.java deleted file mode 100644 index eece48a..0000000 --- a/src/com/android/messaging/util/exif/ExifReader.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2012 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.android.messaging.util.exif; - -import android.util.Log; -import com.android.messaging.util.LogUtil; - -import java.io.IOException; -import java.io.InputStream; - -/** - * This class reads the EXIF header of a JPEG file and stores it in - * {@link ExifData}. - */ -class ExifReader { - private static final String TAG = LogUtil.BUGLE_TAG; - - private final ExifInterface mInterface; - - ExifReader(ExifInterface iRef) { - mInterface = iRef; - } - - /** - * Parses the inputStream and and returns the EXIF data in an - * {@link ExifData}. - * - * @throws ExifInvalidFormatException - * @throws java.io.IOException - */ - protected ExifData read(InputStream inputStream) throws ExifInvalidFormatException, - IOException { - ExifParser parser = ExifParser.parse(inputStream, mInterface); - ExifData exifData = new ExifData(parser.getByteOrder()); - ExifTag tag = null; - - int event = parser.next(); - while (event != ExifParser.EVENT_END) { - switch (event) { - case ExifParser.EVENT_START_OF_IFD: - exifData.addIfdData(new IfdData(parser.getCurrentIfd())); - break; - case ExifParser.EVENT_NEW_TAG: - tag = parser.getTag(); - if (!tag.hasValue()) { - parser.registerForTagValue(tag); - } else { - exifData.getIfdData(tag.getIfd()).setTag(tag); - } - break; - case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG: - tag = parser.getTag(); - if (tag.getDataType() == ExifTag.TYPE_UNDEFINED) { - parser.readFullTagValue(tag); - } - exifData.getIfdData(tag.getIfd()).setTag(tag); - break; - case ExifParser.EVENT_COMPRESSED_IMAGE: - byte buf[] = new byte[parser.getCompressedImageSize()]; - if (buf.length == parser.read(buf)) { - exifData.setCompressedThumbnail(buf); - } else { - Log.w(TAG, "Failed to read the compressed thumbnail"); - } - break; - case ExifParser.EVENT_UNCOMPRESSED_STRIP: - buf = new byte[parser.getStripSize()]; - if (buf.length == parser.read(buf)) { - exifData.setStripBytes(parser.getStripIndex(), buf); - } else { - Log.w(TAG, "Failed to read the strip bytes"); - } - break; - } - event = parser.next(); - } - return exifData; - } -} diff --git a/src/com/android/messaging/util/exif/ExifTag.java b/src/com/android/messaging/util/exif/ExifTag.java deleted file mode 100644 index da6f4ed..0000000 --- a/src/com/android/messaging/util/exif/ExifTag.java +++ /dev/null @@ -1,1008 +0,0 @@ -/* - * Copyright (C) 2012 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.android.messaging.util.exif; - -import java.nio.charset.Charset; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Date; - -/** - * This class stores information of an EXIF tag. For more information about - * defined EXIF tags, please read the Jeita EXIF 2.2 standard. Tags should be - * instantiated using {@link ExifInterface#buildTag}. - * - * @see ExifInterface - */ -public class ExifTag { - /** - * The BYTE type in the EXIF standard. An 8-bit unsigned integer. - */ - public static final short TYPE_UNSIGNED_BYTE = 1; - /** - * The ASCII type in the EXIF standard. An 8-bit byte containing one 7-bit - * ASCII code. The final byte is terminated with NULL. - */ - public static final short TYPE_ASCII = 2; - /** - * The SHORT type in the EXIF standard. A 16-bit (2-byte) unsigned integer - */ - public static final short TYPE_UNSIGNED_SHORT = 3; - /** - * The LONG type in the EXIF standard. A 32-bit (4-byte) unsigned integer - */ - public static final short TYPE_UNSIGNED_LONG = 4; - /** - * The RATIONAL type of EXIF standard. It consists of two LONGs. The first - * one is the numerator and the second one expresses the denominator. - */ - public static final short TYPE_UNSIGNED_RATIONAL = 5; - /** - * The UNDEFINED type in the EXIF standard. An 8-bit byte that can take any - * value depending on the field definition. - */ - public static final short TYPE_UNDEFINED = 7; - /** - * The SLONG type in the EXIF standard. A 32-bit (4-byte) signed integer - * (2's complement notation). - */ - public static final short TYPE_LONG = 9; - /** - * The SRATIONAL type of EXIF standard. It consists of two SLONGs. The first - * one is the numerator and the second one is the denominator. - */ - public static final short TYPE_RATIONAL = 10; - - private static Charset US_ASCII = Charset.forName("US-ASCII"); - private static final int TYPE_TO_SIZE_MAP[] = new int[11]; - private static final int UNSIGNED_SHORT_MAX = 65535; - private static final long UNSIGNED_LONG_MAX = 4294967295L; - private static final long LONG_MAX = Integer.MAX_VALUE; - private static final long LONG_MIN = Integer.MIN_VALUE; - - static { - TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_BYTE] = 1; - TYPE_TO_SIZE_MAP[TYPE_ASCII] = 1; - TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_SHORT] = 2; - TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_LONG] = 4; - TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_RATIONAL] = 8; - TYPE_TO_SIZE_MAP[TYPE_UNDEFINED] = 1; - TYPE_TO_SIZE_MAP[TYPE_LONG] = 4; - TYPE_TO_SIZE_MAP[TYPE_RATIONAL] = 8; - } - - static final int SIZE_UNDEFINED = 0; - - // Exif TagId - private final short mTagId; - // Exif Tag Type - private final short mDataType; - // If tag has defined count - private boolean mHasDefinedDefaultComponentCount; - // Actual data count in tag (should be number of elements in value array) - private int mComponentCountActual; - // The ifd that this tag should be put in - private int mIfd; - // The value (array of elements of type Tag Type) - private Object mValue; - // Value offset in exif header. - private int mOffset; - - private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("yyyy:MM:dd kk:mm:ss"); - - /** - * Returns true if the given IFD is a valid IFD. - */ - public static boolean isValidIfd(int ifdId) { - return ifdId == IfdId.TYPE_IFD_0 || ifdId == IfdId.TYPE_IFD_1 - || ifdId == IfdId.TYPE_IFD_EXIF || ifdId == IfdId.TYPE_IFD_INTEROPERABILITY - || ifdId == IfdId.TYPE_IFD_GPS; - } - - /** - * Returns true if a given type is a valid tag type. - */ - public static boolean isValidType(short type) { - return type == TYPE_UNSIGNED_BYTE || type == TYPE_ASCII || - type == TYPE_UNSIGNED_SHORT || type == TYPE_UNSIGNED_LONG || - type == TYPE_UNSIGNED_RATIONAL || type == TYPE_UNDEFINED || - type == TYPE_LONG || type == TYPE_RATIONAL; - } - - // Use builtTag in ExifInterface instead of constructor. - ExifTag(short tagId, short type, int componentCount, int ifd, - boolean hasDefinedComponentCount) { - mTagId = tagId; - mDataType = type; - mComponentCountActual = componentCount; - mHasDefinedDefaultComponentCount = hasDefinedComponentCount; - mIfd = ifd; - mValue = null; - } - - /** - * Gets the element size of the given data type in bytes. - * - * @see #TYPE_ASCII - * @see #TYPE_LONG - * @see #TYPE_RATIONAL - * @see #TYPE_UNDEFINED - * @see #TYPE_UNSIGNED_BYTE - * @see #TYPE_UNSIGNED_LONG - * @see #TYPE_UNSIGNED_RATIONAL - * @see #TYPE_UNSIGNED_SHORT - */ - public static int getElementSize(short type) { - return TYPE_TO_SIZE_MAP[type]; - } - - /** - * Returns the ID of the IFD this tag belongs to. - * - * @see IfdId#TYPE_IFD_0 - * @see IfdId#TYPE_IFD_1 - * @see IfdId#TYPE_IFD_EXIF - * @see IfdId#TYPE_IFD_GPS - * @see IfdId#TYPE_IFD_INTEROPERABILITY - */ - public int getIfd() { - return mIfd; - } - - protected void setIfd(int ifdId) { - mIfd = ifdId; - } - - /** - * Gets the TID of this tag. - */ - public short getTagId() { - return mTagId; - } - - /** - * Gets the data type of this tag - * - * @see #TYPE_ASCII - * @see #TYPE_LONG - * @see #TYPE_RATIONAL - * @see #TYPE_UNDEFINED - * @see #TYPE_UNSIGNED_BYTE - * @see #TYPE_UNSIGNED_LONG - * @see #TYPE_UNSIGNED_RATIONAL - * @see #TYPE_UNSIGNED_SHORT - */ - public short getDataType() { - return mDataType; - } - - /** - * Gets the total data size in bytes of the value of this tag. - */ - public int getDataSize() { - return getComponentCount() * getElementSize(getDataType()); - } - - /** - * Gets the component count of this tag. - */ - - // TODO: fix integer overflows with this - public int getComponentCount() { - return mComponentCountActual; - } - - /** - * Sets the component count of this tag. Call this function before - * setValue() if the length of value does not match the component count. - */ - protected void forceSetComponentCount(int count) { - mComponentCountActual = count; - } - - /** - * Returns true if this ExifTag contains value; otherwise, this tag will - * contain an offset value that is determined when the tag is written. - */ - public boolean hasValue() { - return mValue != null; - } - - /** - * Sets integer values into this tag. This method should be used for tags of - * type {@link #TYPE_UNSIGNED_SHORT}. This method will fail if: - * <ul> - * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT}, - * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.</li> - * <li>The value overflows.</li> - * <li>The value.length does NOT match the component count in the definition - * for this tag.</li> - * </ul> - */ - public boolean setValue(int[] value) { - if (checkBadComponentCount(value.length)) { - return false; - } - if (mDataType != TYPE_UNSIGNED_SHORT && mDataType != TYPE_LONG && - mDataType != TYPE_UNSIGNED_LONG) { - return false; - } - if (mDataType == TYPE_UNSIGNED_SHORT && checkOverflowForUnsignedShort(value)) { - return false; - } else if (mDataType == TYPE_UNSIGNED_LONG && checkOverflowForUnsignedLong(value)) { - return false; - } - - long[] data = new long[value.length]; - for (int i = 0; i < value.length; i++) { - data[i] = value[i]; - } - mValue = data; - mComponentCountActual = value.length; - return true; - } - - /** - * Sets integer value into this tag. This method should be used for tags of - * type {@link #TYPE_UNSIGNED_SHORT}, or {@link #TYPE_LONG}. This method - * will fail if: - * <ul> - * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT}, - * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.</li> - * <li>The value overflows.</li> - * <li>The component count in the definition of this tag is not 1.</li> - * </ul> - */ - public boolean setValue(int value) { - return setValue(new int[] { - value - }); - } - - /** - * Sets long values into this tag. This method should be used for tags of - * type {@link #TYPE_UNSIGNED_LONG}. This method will fail if: - * <ul> - * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.</li> - * <li>The value overflows.</li> - * <li>The value.length does NOT match the component count in the definition - * for this tag.</li> - * </ul> - */ - public boolean setValue(long[] value) { - if (checkBadComponentCount(value.length) || mDataType != TYPE_UNSIGNED_LONG) { - return false; - } - if (checkOverflowForUnsignedLong(value)) { - return false; - } - mValue = value; - mComponentCountActual = value.length; - return true; - } - - /** - * Sets long values into this tag. This method should be used for tags of - * type {@link #TYPE_UNSIGNED_LONG}. This method will fail if: - * <ul> - * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.</li> - * <li>The value overflows.</li> - * <li>The component count in the definition for this tag is not 1.</li> - * </ul> - */ - public boolean setValue(long value) { - return setValue(new long[] { - value - }); - } - - /** - * Sets a string value into this tag. This method should be used for tags of - * type {@link #TYPE_ASCII}. The string is converted to an ASCII string. - * Characters that cannot be converted are replaced with '?'. The length of - * the string must be equal to either (component count -1) or (component - * count). The final byte will be set to the string null terminator '\0', - * overwriting the last character in the string if the value.length is equal - * to the component count. This method will fail if: - * <ul> - * <li>The data type is not {@link #TYPE_ASCII} or {@link #TYPE_UNDEFINED}.</li> - * <li>The length of the string is not equal to (component count -1) or - * (component count) in the definition for this tag.</li> - * </ul> - */ - public boolean setValue(String value) { - if (mDataType != TYPE_ASCII && mDataType != TYPE_UNDEFINED) { - return false; - } - - byte[] buf = value.getBytes(US_ASCII); - byte[] finalBuf = buf; - if (buf.length > 0) { - finalBuf = (buf[buf.length - 1] == 0 || mDataType == TYPE_UNDEFINED) ? buf : Arrays - .copyOf(buf, buf.length + 1); - } else if (mDataType == TYPE_ASCII && mComponentCountActual == 1) { - finalBuf = new byte[] { 0 }; - } - int count = finalBuf.length; - if (checkBadComponentCount(count)) { - return false; - } - mComponentCountActual = count; - mValue = finalBuf; - return true; - } - - /** - * Sets Rational values into this tag. This method should be used for tags - * of type {@link #TYPE_UNSIGNED_RATIONAL}, or {@link #TYPE_RATIONAL}. This - * method will fail if: - * <ul> - * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL} - * or {@link #TYPE_RATIONAL}.</li> - * <li>The value overflows.</li> - * <li>The value.length does NOT match the component count in the definition - * for this tag.</li> - * </ul> - * - * @see Rational - */ - public boolean setValue(Rational[] value) { - if (checkBadComponentCount(value.length)) { - return false; - } - if (mDataType != TYPE_UNSIGNED_RATIONAL && mDataType != TYPE_RATIONAL) { - return false; - } - if (mDataType == TYPE_UNSIGNED_RATIONAL && checkOverflowForUnsignedRational(value)) { - return false; - } else if (mDataType == TYPE_RATIONAL && checkOverflowForRational(value)) { - return false; - } - - mValue = value; - mComponentCountActual = value.length; - return true; - } - - /** - * Sets a Rational value into this tag. This method should be used for tags - * of type {@link #TYPE_UNSIGNED_RATIONAL}, or {@link #TYPE_RATIONAL}. This - * method will fail if: - * <ul> - * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL} - * or {@link #TYPE_RATIONAL}.</li> - * <li>The value overflows.</li> - * <li>The component count in the definition for this tag is not 1.</li> - * </ul> - * - * @see Rational - */ - public boolean setValue(Rational value) { - return setValue(new Rational[] { - value - }); - } - - /** - * Sets byte values into this tag. This method should be used for tags of - * type {@link #TYPE_UNSIGNED_BYTE} or {@link #TYPE_UNDEFINED}. This method - * will fail if: - * <ul> - * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or - * {@link #TYPE_UNDEFINED} .</li> - * <li>The length does NOT match the component count in the definition for - * this tag.</li> - * </ul> - */ - public boolean setValue(byte[] value, int offset, int length) { - if (checkBadComponentCount(length)) { - return false; - } - if (mDataType != TYPE_UNSIGNED_BYTE && mDataType != TYPE_UNDEFINED) { - return false; - } - mValue = new byte[length]; - System.arraycopy(value, offset, mValue, 0, length); - mComponentCountActual = length; - return true; - } - - /** - * Equivalent to setValue(value, 0, value.length). - */ - public boolean setValue(byte[] value) { - return setValue(value, 0, value.length); - } - - /** - * Sets byte value into this tag. This method should be used for tags of - * type {@link #TYPE_UNSIGNED_BYTE} or {@link #TYPE_UNDEFINED}. This method - * will fail if: - * <ul> - * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or - * {@link #TYPE_UNDEFINED} .</li> - * <li>The component count in the definition for this tag is not 1.</li> - * </ul> - */ - public boolean setValue(byte value) { - return setValue(new byte[] { - value - }); - } - - /** - * Sets the value for this tag using an appropriate setValue method for the - * given object. This method will fail if: - * <ul> - * <li>The corresponding setValue method for the class of the object passed - * in would fail.</li> - * <li>There is no obvious way to cast the object passed in into an EXIF tag - * type.</li> - * </ul> - */ - public boolean setValue(Object obj) { - if (obj == null) { - return false; - } else if (obj instanceof Short) { - return setValue(((Short) obj).shortValue() & 0x0ffff); - } else if (obj instanceof String) { - return setValue((String) obj); - } else if (obj instanceof int[]) { - return setValue((int[]) obj); - } else if (obj instanceof long[]) { - return setValue((long[]) obj); - } else if (obj instanceof Rational) { - return setValue((Rational) obj); - } else if (obj instanceof Rational[]) { - return setValue((Rational[]) obj); - } else if (obj instanceof byte[]) { - return setValue((byte[]) obj); - } else if (obj instanceof Integer) { - return setValue(((Integer) obj).intValue()); - } else if (obj instanceof Long) { - return setValue(((Long) obj).longValue()); - } else if (obj instanceof Byte) { - return setValue(((Byte) obj).byteValue()); - } else if (obj instanceof Short[]) { - // Nulls in this array are treated as zeroes. - Short[] arr = (Short[]) obj; - int[] fin = new int[arr.length]; - for (int i = 0; i < arr.length; i++) { - fin[i] = (arr[i] == null) ? 0 : arr[i].shortValue() & 0x0ffff; - } - return setValue(fin); - } else if (obj instanceof Integer[]) { - // Nulls in this array are treated as zeroes. - Integer[] arr = (Integer[]) obj; - int[] fin = new int[arr.length]; - for (int i = 0; i < arr.length; i++) { - fin[i] = (arr[i] == null) ? 0 : arr[i].intValue(); - } - return setValue(fin); - } else if (obj instanceof Long[]) { - // Nulls in this array are treated as zeroes. - Long[] arr = (Long[]) obj; - long[] fin = new long[arr.length]; - for (int i = 0; i < arr.length; i++) { - fin[i] = (arr[i] == null) ? 0 : arr[i].longValue(); - } - return setValue(fin); - } else if (obj instanceof Byte[]) { - // Nulls in this array are treated as zeroes. - Byte[] arr = (Byte[]) obj; - byte[] fin = new byte[arr.length]; - for (int i = 0; i < arr.length; i++) { - fin[i] = (arr[i] == null) ? 0 : arr[i].byteValue(); - } - return setValue(fin); - } else { - return false; - } - } - - /** - * Sets a timestamp to this tag. The method converts the timestamp with the - * format of "yyyy:MM:dd kk:mm:ss" and calls {@link #setValue(String)}. This - * method will fail if the data type is not {@link #TYPE_ASCII} or the - * component count of this tag is not 20 or undefined. - * - * @param time the number of milliseconds since Jan. 1, 1970 GMT - * @return true on success - */ - public boolean setTimeValue(long time) { - // synchronized on TIME_FORMAT as SimpleDateFormat is not thread safe - synchronized (TIME_FORMAT) { - return setValue(TIME_FORMAT.format(new Date(time))); - } - } - - /** - * Gets the value as a String. This method should be used for tags of type - * {@link #TYPE_ASCII}. - * - * @return the value as a String, or null if the tag's value does not exist - * or cannot be converted to a String. - */ - public String getValueAsString() { - if (mValue == null) { - return null; - } else if (mValue instanceof String) { - return (String) mValue; - } else if (mValue instanceof byte[]) { - return new String((byte[]) mValue, US_ASCII); - } - return null; - } - - /** - * Gets the value as a String. This method should be used for tags of type - * {@link #TYPE_ASCII}. - * - * @param defaultValue the String to return if the tag's value does not - * exist or cannot be converted to a String. - * @return the tag's value as a String, or the defaultValue. - */ - public String getValueAsString(String defaultValue) { - String s = getValueAsString(); - if (s == null) { - return defaultValue; - } - return s; - } - - /** - * Gets the value as a byte array. This method should be used for tags of - * type {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE}. - * - * @return the value as a byte array, or null if the tag's value does not - * exist or cannot be converted to a byte array. - */ - public byte[] getValueAsBytes() { - if (mValue instanceof byte[]) { - return (byte[]) mValue; - } - return null; - } - - /** - * Gets the value as a byte. If there are more than 1 bytes in this value, - * gets the first byte. This method should be used for tags of type - * {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE}. - * - * @param defaultValue the byte to return if tag's value does not exist or - * cannot be converted to a byte. - * @return the tag's value as a byte, or the defaultValue. - */ - public byte getValueAsByte(byte defaultValue) { - byte[] b = getValueAsBytes(); - if (b == null || b.length < 1) { - return defaultValue; - } - return b[0]; - } - - /** - * Gets the value as an array of Rationals. This method should be used for - * tags of type {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}. - * - * @return the value as as an array of Rationals, or null if the tag's value - * does not exist or cannot be converted to an array of Rationals. - */ - public Rational[] getValueAsRationals() { - if (mValue instanceof Rational[]) { - return (Rational[]) mValue; - } - return null; - } - - /** - * Gets the value as a Rational. If there are more than 1 Rationals in this - * value, gets the first one. This method should be used for tags of type - * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}. - * - * @param defaultValue the Rational to return if tag's value does not exist - * or cannot be converted to a Rational. - * @return the tag's value as a Rational, or the defaultValue. - */ - public Rational getValueAsRational(Rational defaultValue) { - Rational[] r = getValueAsRationals(); - if (r == null || r.length < 1) { - return defaultValue; - } - return r[0]; - } - - /** - * Gets the value as a Rational. If there are more than 1 Rationals in this - * value, gets the first one. This method should be used for tags of type - * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}. - * - * @param defaultValue the numerator of the Rational to return if tag's - * value does not exist or cannot be converted to a Rational (the - * denominator will be 1). - * @return the tag's value as a Rational, or the defaultValue. - */ - public Rational getValueAsRational(long defaultValue) { - Rational defaultVal = new Rational(defaultValue, 1); - return getValueAsRational(defaultVal); - } - - /** - * Gets the value as an array of ints. This method should be used for tags - * of type {@link #TYPE_UNSIGNED_SHORT}, {@link #TYPE_UNSIGNED_LONG}. - * - * @return the value as as an array of ints, or null if the tag's value does - * not exist or cannot be converted to an array of ints. - */ - public int[] getValueAsInts() { - if (mValue == null) { - return null; - } else if (mValue instanceof long[]) { - long[] val = (long[]) mValue; - int[] arr = new int[val.length]; - for (int i = 0; i < val.length; i++) { - arr[i] = (int) val[i]; // Truncates - } - return arr; - } - return null; - } - - /** - * Gets the value as an int. If there are more than 1 ints in this value, - * gets the first one. This method should be used for tags of type - * {@link #TYPE_UNSIGNED_SHORT}, {@link #TYPE_UNSIGNED_LONG}. - * - * @param defaultValue the int to return if tag's value does not exist or - * cannot be converted to an int. - * @return the tag's value as a int, or the defaultValue. - */ - public int getValueAsInt(int defaultValue) { - int[] i = getValueAsInts(); - if (i == null || i.length < 1) { - return defaultValue; - } - return i[0]; - } - - /** - * Gets the value as an array of longs. This method should be used for tags - * of type {@link #TYPE_UNSIGNED_LONG}. - * - * @return the value as as an array of longs, or null if the tag's value - * does not exist or cannot be converted to an array of longs. - */ - public long[] getValueAsLongs() { - if (mValue instanceof long[]) { - return (long[]) mValue; - } - return null; - } - - /** - * Gets the value or null if none exists. If there are more than 1 longs in - * this value, gets the first one. This method should be used for tags of - * type {@link #TYPE_UNSIGNED_LONG}. - * - * @param defaultValue the long to return if tag's value does not exist or - * cannot be converted to a long. - * @return the tag's value as a long, or the defaultValue. - */ - public long getValueAsLong(long defaultValue) { - long[] l = getValueAsLongs(); - if (l == null || l.length < 1) { - return defaultValue; - } - return l[0]; - } - - /** - * Gets the tag's value or null if none exists. - */ - public Object getValue() { - return mValue; - } - - /** - * Gets a long representation of the value. - * - * @param defaultValue value to return if there is no value or value is a - * rational with a denominator of 0. - * @return the tag's value as a long, or defaultValue if no representation - * exists. - */ - public long forceGetValueAsLong(long defaultValue) { - long[] l = getValueAsLongs(); - if (l != null && l.length >= 1) { - return l[0]; - } - byte[] b = getValueAsBytes(); - if (b != null && b.length >= 1) { - return b[0]; - } - Rational[] r = getValueAsRationals(); - if (r != null && r.length >= 1 && r[0].getDenominator() != 0) { - return (long) r[0].toDouble(); - } - return defaultValue; - } - - /** - * Gets a string representation of the value. - */ - public String forceGetValueAsString() { - if (mValue == null) { - return ""; - } else if (mValue instanceof byte[]) { - if (mDataType == TYPE_ASCII) { - return new String((byte[]) mValue, US_ASCII); - } else { - return Arrays.toString((byte[]) mValue); - } - } else if (mValue instanceof long[]) { - if (((long[]) mValue).length == 1) { - return String.valueOf(((long[]) mValue)[0]); - } else { - return Arrays.toString((long[]) mValue); - } - } else if (mValue instanceof Object[]) { - if (((Object[]) mValue).length == 1) { - Object val = ((Object[]) mValue)[0]; - if (val == null) { - return ""; - } else { - return val.toString(); - } - } else { - return Arrays.toString((Object[]) mValue); - } - } else { - return mValue.toString(); - } - } - - /** - * Gets the value for type {@link #TYPE_ASCII}, {@link #TYPE_LONG}, - * {@link #TYPE_UNDEFINED}, {@link #TYPE_UNSIGNED_BYTE}, - * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_UNSIGNED_SHORT}. For - * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}, call - * {@link #getRational(int)} instead. - * - * @exception IllegalArgumentException if the data type is - * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}. - */ - protected long getValueAt(int index) { - if (mValue instanceof long[]) { - return ((long[]) mValue)[index]; - } else if (mValue instanceof byte[]) { - return ((byte[]) mValue)[index]; - } - throw new IllegalArgumentException("Cannot get integer value from " - + convertTypeToString(mDataType)); - } - - /** - * Gets the {@link #TYPE_ASCII} data. - * - * @exception IllegalArgumentException If the type is NOT - * {@link #TYPE_ASCII}. - */ - protected String getString() { - if (mDataType != TYPE_ASCII) { - throw new IllegalArgumentException("Cannot get ASCII value from " - + convertTypeToString(mDataType)); - } - return new String((byte[]) mValue, US_ASCII); - } - - /* - * Get the converted ascii byte. Used by ExifOutputStream. - */ - protected byte[] getStringByte() { - return (byte[]) mValue; - } - - /** - * Gets the {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL} data. - * - * @exception IllegalArgumentException If the type is NOT - * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}. - */ - protected Rational getRational(int index) { - if ((mDataType != TYPE_RATIONAL) && (mDataType != TYPE_UNSIGNED_RATIONAL)) { - throw new IllegalArgumentException("Cannot get RATIONAL value from " - + convertTypeToString(mDataType)); - } - return ((Rational[]) mValue)[index]; - } - - /** - * Equivalent to getBytes(buffer, 0, buffer.length). - */ - protected void getBytes(byte[] buf) { - getBytes(buf, 0, buf.length); - } - - /** - * Gets the {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE} data. - * - * @param buf the byte array in which to store the bytes read. - * @param offset the initial position in buffer to store the bytes. - * @param length the maximum number of bytes to store in buffer. If length > - * component count, only the valid bytes will be stored. - * @exception IllegalArgumentException If the type is NOT - * {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE}. - */ - protected void getBytes(byte[] buf, int offset, int length) { - if ((mDataType != TYPE_UNDEFINED) && (mDataType != TYPE_UNSIGNED_BYTE)) { - throw new IllegalArgumentException("Cannot get BYTE value from " - + convertTypeToString(mDataType)); - } - System.arraycopy(mValue, 0, buf, offset, - (length > mComponentCountActual) ? mComponentCountActual : length); - } - - /** - * Gets the offset of this tag. This is only valid if this data size > 4 and - * contains an offset to the location of the actual value. - */ - protected int getOffset() { - return mOffset; - } - - /** - * Sets the offset of this tag. - */ - protected void setOffset(int offset) { - mOffset = offset; - } - - protected void setHasDefinedCount(boolean d) { - mHasDefinedDefaultComponentCount = d; - } - - protected boolean hasDefinedCount() { - return mHasDefinedDefaultComponentCount; - } - - private boolean checkBadComponentCount(int count) { - if (mHasDefinedDefaultComponentCount && (mComponentCountActual != count)) { - return true; - } - return false; - } - - private static String convertTypeToString(short type) { - switch (type) { - case TYPE_UNSIGNED_BYTE: - return "UNSIGNED_BYTE"; - case TYPE_ASCII: - return "ASCII"; - case TYPE_UNSIGNED_SHORT: - return "UNSIGNED_SHORT"; - case TYPE_UNSIGNED_LONG: - return "UNSIGNED_LONG"; - case TYPE_UNSIGNED_RATIONAL: - return "UNSIGNED_RATIONAL"; - case TYPE_UNDEFINED: - return "UNDEFINED"; - case TYPE_LONG: - return "LONG"; - case TYPE_RATIONAL: - return "RATIONAL"; - default: - return ""; - } - } - - private boolean checkOverflowForUnsignedShort(int[] value) { - for (int v : value) { - if (v > UNSIGNED_SHORT_MAX || v < 0) { - return true; - } - } - return false; - } - - private boolean checkOverflowForUnsignedLong(long[] value) { - for (long v : value) { - if (v < 0 || v > UNSIGNED_LONG_MAX) { - return true; - } - } - return false; - } - - private boolean checkOverflowForUnsignedLong(int[] value) { - for (int v : value) { - if (v < 0) { - return true; - } - } - return false; - } - - private boolean checkOverflowForUnsignedRational(Rational[] value) { - for (Rational v : value) { - if (v.getNumerator() < 0 || v.getDenominator() < 0 - || v.getNumerator() > UNSIGNED_LONG_MAX - || v.getDenominator() > UNSIGNED_LONG_MAX) { - return true; - } - } - return false; - } - - private boolean checkOverflowForRational(Rational[] value) { - for (Rational v : value) { - if (v.getNumerator() < LONG_MIN || v.getDenominator() < LONG_MIN - || v.getNumerator() > LONG_MAX - || v.getDenominator() > LONG_MAX) { - return true; - } - } - return false; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (obj instanceof ExifTag) { - ExifTag tag = (ExifTag) obj; - if (tag.mTagId != this.mTagId - || tag.mComponentCountActual != this.mComponentCountActual - || tag.mDataType != this.mDataType) { - return false; - } - if (mValue != null) { - if (tag.mValue == null) { - return false; - } else if (mValue instanceof long[]) { - if (!(tag.mValue instanceof long[])) { - return false; - } - return Arrays.equals((long[]) mValue, (long[]) tag.mValue); - } else if (mValue instanceof Rational[]) { - if (!(tag.mValue instanceof Rational[])) { - return false; - } - return Arrays.equals((Rational[]) mValue, (Rational[]) tag.mValue); - } else if (mValue instanceof byte[]) { - if (!(tag.mValue instanceof byte[])) { - return false; - } - return Arrays.equals((byte[]) mValue, (byte[]) tag.mValue); - } else { - return mValue.equals(tag.mValue); - } - } else { - return tag.mValue == null; - } - } - return false; - } - - @Override - public String toString() { - return String.format("tag id: %04X\n", mTagId) + "ifd id: " + mIfd + "\ntype: " - + convertTypeToString(mDataType) + "\ncount: " + mComponentCountActual - + "\noffset: " + mOffset + "\nvalue: " + forceGetValueAsString() + "\n"; - } - -} diff --git a/src/com/android/messaging/util/exif/IfdData.java b/src/com/android/messaging/util/exif/IfdData.java deleted file mode 100644 index 6b8c293..0000000 --- a/src/com/android/messaging/util/exif/IfdData.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (C) 2012 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.android.messaging.util.exif; - -import java.util.HashMap; -import java.util.Map; - -/** - * This class stores all the tags in an IFD. - * - * @see ExifData - * @see ExifTag - */ -class IfdData { - - private final int mIfdId; - private final Map<Short, ExifTag> mExifTags = new HashMap<Short, ExifTag>(); - private int mOffsetToNextIfd = 0; - private static final int[] sIfds = { - IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1, IfdId.TYPE_IFD_EXIF, - IfdId.TYPE_IFD_INTEROPERABILITY, IfdId.TYPE_IFD_GPS - }; - /** - * Creates an IfdData with given IFD ID. - * - * @see IfdId#TYPE_IFD_0 - * @see IfdId#TYPE_IFD_1 - * @see IfdId#TYPE_IFD_EXIF - * @see IfdId#TYPE_IFD_GPS - * @see IfdId#TYPE_IFD_INTEROPERABILITY - */ - IfdData(int ifdId) { - mIfdId = ifdId; - } - - protected static int[] getIfds() { - return sIfds; - } - - /** - * Get a array the contains all {@link ExifTag} in this IFD. - */ - protected ExifTag[] getAllTags() { - return mExifTags.values().toArray(new ExifTag[mExifTags.size()]); - } - - /** - * Gets the ID of this IFD. - * - * @see IfdId#TYPE_IFD_0 - * @see IfdId#TYPE_IFD_1 - * @see IfdId#TYPE_IFD_EXIF - * @see IfdId#TYPE_IFD_GPS - * @see IfdId#TYPE_IFD_INTEROPERABILITY - */ - protected int getId() { - return mIfdId; - } - - /** - * Gets the {@link ExifTag} with given tag id. Return null if there is no - * such tag. - */ - protected ExifTag getTag(short tagId) { - return mExifTags.get(tagId); - } - - /** - * Adds or replaces a {@link ExifTag}. - */ - protected ExifTag setTag(ExifTag tag) { - tag.setIfd(mIfdId); - return mExifTags.put(tag.getTagId(), tag); - } - - protected boolean checkCollision(short tagId) { - return mExifTags.get(tagId) != null; - } - - /** - * Removes the tag of the given ID - */ - protected void removeTag(short tagId) { - mExifTags.remove(tagId); - } - - /** - * Gets the tags count in the IFD. - */ - protected int getTagCount() { - return mExifTags.size(); - } - - /** - * Sets the offset of next IFD. - */ - protected void setOffsetToNextIfd(int offset) { - mOffsetToNextIfd = offset; - } - - /** - * Gets the offset of next IFD. - */ - protected int getOffsetToNextIfd() { - return mOffsetToNextIfd; - } - - /** - * Returns true if all tags in this two IFDs are equal. Note that tags of - * IFDs offset or thumbnail offset will be ignored. - */ - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (obj instanceof IfdData) { - IfdData data = (IfdData) obj; - if (data.getId() == mIfdId && data.getTagCount() == getTagCount()) { - ExifTag[] tags = data.getAllTags(); - for (ExifTag tag : tags) { - if (ExifInterface.isOffsetTag(tag.getTagId())) { - continue; - } - ExifTag tag2 = mExifTags.get(tag.getTagId()); - if (!tag.equals(tag2)) { - return false; - } - } - return true; - } - } - return false; - } -} diff --git a/src/com/android/messaging/util/exif/IfdId.java b/src/com/android/messaging/util/exif/IfdId.java deleted file mode 100644 index 06a820d..0000000 --- a/src/com/android/messaging/util/exif/IfdId.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2012 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.android.messaging.util.exif; - -/** - * The constants of the IFD ID defined in EXIF spec. - */ -public interface IfdId { - public static final int TYPE_IFD_0 = 0; - public static final int TYPE_IFD_1 = 1; - public static final int TYPE_IFD_EXIF = 2; - public static final int TYPE_IFD_INTEROPERABILITY = 3; - public static final int TYPE_IFD_GPS = 4; - /* This is used in ExifData to allocate enough IfdData */ - static final int TYPE_IFD_COUNT = 5; - -} diff --git a/src/com/android/messaging/util/exif/JpegHeader.java b/src/com/android/messaging/util/exif/JpegHeader.java deleted file mode 100644 index 1dd12a5..0000000 --- a/src/com/android/messaging/util/exif/JpegHeader.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2012 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.android.messaging.util.exif; - -class JpegHeader { - public static final short SOI = (short) 0xFFD8; - public static final short APP1 = (short) 0xFFE1; - public static final short APP0 = (short) 0xFFE0; - public static final short EOI = (short) 0xFFD9; - - /** - * SOF (start of frame). All value between SOF0 and SOF15 is SOF marker except for DHT, JPG, - * and DAC marker. - */ - public static final short SOF0 = (short) 0xFFC0; - public static final short SOF15 = (short) 0xFFCF; - public static final short DHT = (short) 0xFFC4; - public static final short JPG = (short) 0xFFC8; - public static final short DAC = (short) 0xFFCC; - - public static final boolean isSofMarker(short marker) { - return marker >= SOF0 && marker <= SOF15 && marker != DHT && marker != JPG - && marker != DAC; - } -} diff --git a/src/com/android/messaging/util/exif/OrderedDataOutputStream.java b/src/com/android/messaging/util/exif/OrderedDataOutputStream.java deleted file mode 100644 index cf64805..0000000 --- a/src/com/android/messaging/util/exif/OrderedDataOutputStream.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2012 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.android.messaging.util.exif; - -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -class OrderedDataOutputStream extends FilterOutputStream { - private final ByteBuffer mByteBuffer = ByteBuffer.allocate(4); - - public OrderedDataOutputStream(OutputStream out) { - super(out); - } - - public OrderedDataOutputStream setByteOrder(ByteOrder order) { - mByteBuffer.order(order); - return this; - } - - public OrderedDataOutputStream writeShort(short value) throws IOException { - mByteBuffer.rewind(); - mByteBuffer.putShort(value); - out.write(mByteBuffer.array(), 0, 2); - return this; - } - - public OrderedDataOutputStream writeInt(int value) throws IOException { - mByteBuffer.rewind(); - mByteBuffer.putInt(value); - out.write(mByteBuffer.array()); - return this; - } - - public OrderedDataOutputStream writeRational(Rational rational) throws IOException { - writeInt((int) rational.getNumerator()); - writeInt((int) rational.getDenominator()); - return this; - } -} diff --git a/src/com/android/messaging/util/exif/Rational.java b/src/com/android/messaging/util/exif/Rational.java deleted file mode 100644 index b42ceb7..0000000 --- a/src/com/android/messaging/util/exif/Rational.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2012 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.android.messaging.util.exif; - -/** - * The rational data type of EXIF tag. Contains a pair of longs representing the - * numerator and denominator of a Rational number. - */ -public class Rational { - - private final long mNumerator; - private final long mDenominator; - - /** - * Create a Rational with a given numerator and denominator. - * - * @param nominator - * @param denominator - */ - public Rational(long nominator, long denominator) { - mNumerator = nominator; - mDenominator = denominator; - } - - /** - * Create a copy of a Rational. - */ - public Rational(Rational r) { - mNumerator = r.mNumerator; - mDenominator = r.mDenominator; - } - - /** - * Gets the numerator of the rational. - */ - public long getNumerator() { - return mNumerator; - } - - /** - * Gets the denominator of the rational - */ - public long getDenominator() { - return mDenominator; - } - - /** - * Gets the rational value as type double. Will cause a divide-by-zero error - * if the denominator is 0. - */ - public double toDouble() { - return mNumerator / (double) mDenominator; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (this == obj) { - return true; - } - if (obj instanceof Rational) { - Rational data = (Rational) obj; - return mNumerator == data.mNumerator && mDenominator == data.mDenominator; - } - return false; - } - - @Override - public String toString() { - return mNumerator + "/" + mDenominator; - } -} |