Revert "Initial checkin of AOSP Messaging app."

This reverts commit 461a34b466.

Change-Id: Iac4ca77eeaa94989e91dead49a7959c905bd3078
This commit is contained in:
Mike Dodd
2015-08-12 18:48:39 +00:00
parent 461a34b466
commit ef8c7abbcf
1645 changed files with 0 additions and 186271 deletions
@@ -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;
}
}
}
-214
View File
@@ -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());
}
}
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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) {
}
}
@@ -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);
}
@@ -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;
}
}
@@ -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;
}
@@ -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);
}
@@ -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();
}
}
@@ -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";
}
@@ -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);
}
}
}
@@ -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) {
}
}
@@ -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
}
}
}
@@ -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];
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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;
}
}
-280
View File
@@ -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;
}
@@ -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;
}
}
@@ -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&lt;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&lt;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;
}
@@ -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;
}
}
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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 "?";
}
}
}
-274
View File
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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);
}
@@ -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();
}
}
}
@@ -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();
}
}
}
-269
View File
@@ -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);
}
}
@@ -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";
}
File diff suppressed because it is too large Load Diff
@@ -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;
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
-115
View File
@@ -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() {
}
}
}
@@ -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;
}
}
-438
View File
@@ -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);
}
}
-393
View File
@@ -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);
}
}
@@ -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;
}
}
@@ -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");
}
}
}
}
}
@@ -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());
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
}
File diff suppressed because it is too large Load Diff
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
}
@@ -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();
}
}
@@ -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;
}
}
File diff suppressed because it is too large Load Diff
@@ -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;
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}