Initial checkin of AOSP Messaging app.
b/23110861 Change-Id: I9aa980d7569247d6b2ca78f5dcb4502e1eaadb8a
This commit is contained in:
170
src/com/android/messaging/util/AccessibilityUtil.java
Normal file
170
src/com/android/messaging/util/AccessibilityUtil.java
Normal file
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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
src/com/android/messaging/util/Assert.java
Normal file
214
src/com/android/messaging/util/Assert.java
Normal file
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
320
src/com/android/messaging/util/AvatarUriUtil.java
Normal file
320
src/com/android/messaging/util/AvatarUriUtil.java
Normal file
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
88
src/com/android/messaging/util/BugleActivityUtil.java
Normal file
88
src/com/android/messaging/util/BugleActivityUtil.java
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
45
src/com/android/messaging/util/BugleApplicationPrefs.java
Normal file
45
src/com/android/messaging/util/BugleApplicationPrefs.java
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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) {
|
||||
}
|
||||
}
|
||||
72
src/com/android/messaging/util/BugleGservices.java
Normal file
72
src/com/android/messaging/util/BugleGservices.java
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
68
src/com/android/messaging/util/BugleGservicesImpl.java
Normal file
68
src/com/android/messaging/util/BugleGservicesImpl.java
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
298
src/com/android/messaging/util/BugleGservicesKeys.java
Normal file
298
src/com/android/messaging/util/BugleGservicesKeys.java
Normal file
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
140
src/com/android/messaging/util/BuglePrefs.java
Normal file
140
src/com/android/messaging/util/BuglePrefs.java
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
135
src/com/android/messaging/util/BuglePrefsImpl.java
Normal file
135
src/com/android/messaging/util/BuglePrefsImpl.java
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
71
src/com/android/messaging/util/BuglePrefsKeys.java
Normal file
71
src/com/android/messaging/util/BuglePrefsKeys.java
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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";
|
||||
|
||||
}
|
||||
95
src/com/android/messaging/util/BugleSubscriptionPrefs.java
Normal file
95
src/com/android/messaging/util/BugleSubscriptionPrefs.java
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/com/android/messaging/util/BugleWidgetPrefs.java
Normal file
41
src/com/android/messaging/util/BugleWidgetPrefs.java
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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) {
|
||||
}
|
||||
}
|
||||
157
src/com/android/messaging/util/ChangeDefaultSmsAppHelper.java
Normal file
157
src/com/android/messaging/util/ChangeDefaultSmsAppHelper.java
Normal file
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
111
src/com/android/messaging/util/CircularArray.java
Normal file
111
src/com/android/messaging/util/CircularArray.java
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
247
src/com/android/messaging/util/ConnectivityUtil.java
Normal file
247
src/com/android/messaging/util/ConnectivityUtil.java
Normal file
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
115
src/com/android/messaging/util/ContactRecipientEntryUtils.java
Normal file
115
src/com/android/messaging/util/ContactRecipientEntryUtils.java
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
525
src/com/android/messaging/util/ContactUtil.java
Normal file
525
src/com/android/messaging/util/ContactUtil.java
Normal file
@@ -0,0 +1,525 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
185
src/com/android/messaging/util/ContentType.java
Normal file
185
src/com/android/messaging/util/ContentType.java
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/com/android/messaging/util/ConversationIdSet.java
Normal file
69
src/com/android/messaging/util/ConversationIdSet.java
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
120
src/com/android/messaging/util/CubicBezierInterpolator.java
Normal file
120
src/com/android/messaging/util/CubicBezierInterpolator.java
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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
src/com/android/messaging/util/Dates.java
Normal file
280
src/com/android/messaging/util/Dates.java
Normal file
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
425
src/com/android/messaging/util/DebugUtils.java
Normal file
425
src/com/android/messaging/util/DebugUtils.java
Normal file
@@ -0,0 +1,425 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
272
src/com/android/messaging/util/EmailAddress.java
Normal file
272
src/com/android/messaging/util/EmailAddress.java
Normal file
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.util;
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
|
||||
/**
|
||||
* Parsing the email address
|
||||
*/
|
||||
public final class EmailAddress {
|
||||
private static final CharMatcher ANY_WHITESPACE = CharMatcher.anyOf(
|
||||
" \t\n\r\f\u000B\u0085\u2028\u2029\u200D\uFFEF\uFFFD\uFFFE\uFFFF");
|
||||
private static final CharMatcher EMAIL_ALLOWED_CHARS = CharMatcher.inRange((char) 0, (char) 31)
|
||||
.or(CharMatcher.is((char) 127))
|
||||
.or(CharMatcher.anyOf(" @,:<>"))
|
||||
.negate();
|
||||
|
||||
/**
|
||||
* Helper method that checks whether the input text is valid email address.
|
||||
* TODO: This creates a new EmailAddress object each time
|
||||
* Need to make it more lightweight by pulling out the validation code into a static method.
|
||||
*/
|
||||
public static boolean isValidEmail(final String emailText) {
|
||||
return new EmailAddress(emailText).isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the specified email address. Internationalized addresses are treated as invalid.
|
||||
*
|
||||
* @param emailString A string representing just an email address. It should
|
||||
* not contain any other tokens. <code>"Name<foo@example.org>"</code> won't be valid.
|
||||
*/
|
||||
public EmailAddress(final String emailString) {
|
||||
this(emailString, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the specified email address.
|
||||
*
|
||||
* @param emailString A string representing just an email address. It should
|
||||
* not contain any other tokens. <code>"Name<foo@example.org>"</code> won't be valid.
|
||||
* @param i18n Accept an internationalized address if it is true.
|
||||
*/
|
||||
public EmailAddress(final String emailString, final boolean i18n) {
|
||||
allowI18n = i18n;
|
||||
valid = parseEmail(emailString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the specified email address. Internationalized addresses are treated as invalid.
|
||||
*
|
||||
* @param user A string representing the username in the email prior to the '@' symbol
|
||||
* @param host A string representing the host following the '@' symbol
|
||||
*/
|
||||
public EmailAddress(final String user, final String host) {
|
||||
this(user, host, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the specified email address.
|
||||
*
|
||||
* @param user A string representing the username in the email prior to the '@' symbol
|
||||
* @param host A string representing the host following the '@' symbol
|
||||
* @param i18n Accept an internationalized address if it is true.
|
||||
*/
|
||||
public EmailAddress(final String user, final String host, final boolean i18n) {
|
||||
allowI18n = i18n;
|
||||
this.user = user;
|
||||
setHost(host);
|
||||
}
|
||||
|
||||
protected boolean parseEmail(final String emailString) {
|
||||
// check for null
|
||||
if (emailString == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for an '@' character. Get the last one, in case the local part is
|
||||
// quoted. See http://b/1944742.
|
||||
final int atIndex = emailString.lastIndexOf('@');
|
||||
if ((atIndex <= 0) || // no '@' character in the email address
|
||||
// or @ on the first position
|
||||
(atIndex == (emailString.length() - 1))) { // last character, no host
|
||||
return false;
|
||||
}
|
||||
|
||||
user = emailString.substring(0, atIndex);
|
||||
host = emailString.substring(atIndex + 1);
|
||||
|
||||
return isValidInternal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return user + "@" + host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the email address is valid, conforming to current RFC2821 and
|
||||
* RFC2822 guidelines (although some iffy characters, like ! and ;, are
|
||||
* allowed because they are not technically prohibited in the RFC)
|
||||
*/
|
||||
private boolean isValidInternal() {
|
||||
if ((user == null) || (host == null)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((user.length() == 0) || (host.length() == 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for white space in the host
|
||||
if (ANY_WHITESPACE.indexIn(host) >= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ensure the host is above the minimum length
|
||||
if (host.length() < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int firstDot = host.indexOf('.');
|
||||
|
||||
// ensure host contains at least one dot
|
||||
if (firstDot == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if the host contains two continuous dots.
|
||||
if (host.indexOf("..") >= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if the first host char is a dot.
|
||||
if (host.charAt(0) == '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int secondDot = host.indexOf(".", firstDot + 1);
|
||||
|
||||
// if there's a dot at the end, there needs to be a second dot
|
||||
if (host.charAt(host.length() - 1) == '.' && secondDot == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Host must not have any disallowed characters; allowI18n dictates whether
|
||||
// host must be ASCII.
|
||||
if (!EMAIL_ALLOWED_CHARS.matchesAllOf(host)
|
||||
|| (!allowI18n && !CharMatcher.ASCII.matchesAllOf(host))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.startsWith("\"")) {
|
||||
if (!isQuotedUserValid()) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// check for white space in the user
|
||||
if (ANY_WHITESPACE.indexIn(user) >= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// the user cannot contain two continuous dots
|
||||
if (user.indexOf("..") >= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// User must not have any disallowed characters; allow I18n dictates whether
|
||||
// user must be ASCII.
|
||||
if (!EMAIL_ALLOWED_CHARS.matchesAllOf(user)
|
||||
|| (!allowI18n && !CharMatcher.ASCII.matchesAllOf(user))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isQuotedUserValid() {
|
||||
final int limit = user.length() - 1;
|
||||
if (limit < 1 || !user.endsWith("\"")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unusual loop bounds (looking only at characters between the outer quotes,
|
||||
// not at either quote character). Plus, i is manipulated within the loop.
|
||||
for (int i = 1; i < limit; ++i) {
|
||||
final char ch = user.charAt(i);
|
||||
if (ch == '"' || ch == 127
|
||||
// No non-whitespace control chars:
|
||||
|| (ch < 32 && !ANY_WHITESPACE.matches(ch))
|
||||
// No non-ASCII chars, unless i18n is in effect:
|
||||
|| (ch >= 128 && !allowI18n)) {
|
||||
return false;
|
||||
} else if (ch == '\\') {
|
||||
if (i + 1 < limit) {
|
||||
++i; // Skip the quoted character
|
||||
} else {
|
||||
// We have a trailing backslash -- so it can't be quoting anything.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object otherObject) {
|
||||
// Do an instance check first as an optimization.
|
||||
if (this == otherObject) {
|
||||
return true;
|
||||
}
|
||||
if (otherObject instanceof EmailAddress) {
|
||||
final EmailAddress otherAddress = (EmailAddress) otherObject;
|
||||
return toString().equals(otherAddress.toString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Arbitrary hash code as a function of both host and user.
|
||||
return toString().hashCode();
|
||||
}
|
||||
|
||||
// accessors
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
public String getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
// used to change the host on an email address and rechecks validity
|
||||
|
||||
/**
|
||||
* Changes the host name of the email address and rechecks the address'
|
||||
* validity. Exercise caution when storing EmailAddress instances in
|
||||
* hash-keyed collections. Calling setHost() with a different host name will
|
||||
* change the return value of hashCode.
|
||||
*
|
||||
* @param hostName The new host name of the email address.
|
||||
*/
|
||||
public void setHost(final String hostName) {
|
||||
host = hostName;
|
||||
valid = isValidInternal();
|
||||
}
|
||||
|
||||
protected boolean valid = false;
|
||||
protected String user = null;
|
||||
protected String host = null;
|
||||
protected boolean allowI18n = false;
|
||||
}
|
||||
91
src/com/android/messaging/util/FallbackStrategies.java
Normal file
91
src/com/android/messaging/util/FallbackStrategies.java
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
140
src/com/android/messaging/util/FileUtil.java
Normal file
140
src/com/android/messaging/util/FileUtil.java
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src/com/android/messaging/util/GifTranscoder.java
Normal file
94
src/com/android/messaging/util/GifTranscoder.java
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
908
src/com/android/messaging/util/ImageUtils.java
Normal file
908
src/com/android/messaging/util/ImageUtils.java
Normal file
@@ -0,0 +1,908 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
88
src/com/android/messaging/util/ImeUtil.java
Normal file
88
src/com/android/messaging/util/ImeUtil.java
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
293
src/com/android/messaging/util/LogSaver.java
Normal file
293
src/com/android/messaging/util/LogSaver.java
Normal file
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* 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
src/com/android/messaging/util/LogUtil.java
Normal file
274
src/com/android/messaging/util/LogUtil.java
Normal file
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/com/android/messaging/util/LoggingTimer.java
Normal file
70
src/com/android/messaging/util/LoggingTimer.java
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/com/android/messaging/util/LongSparseSet.java
Normal file
59
src/com/android/messaging/util/LongSparseSet.java
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
27
src/com/android/messaging/util/MaterialPalette.java
Normal file
27
src/com/android/messaging/util/MaterialPalette.java
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/com/android/messaging/util/MediaUtil.java
Normal file
36
src/com/android/messaging/util/MediaUtil.java
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
66
src/com/android/messaging/util/MediaUtilImpl.java
Normal file
66
src/com/android/messaging/util/MediaUtilImpl.java
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
363
src/com/android/messaging/util/NotificationPlayer.java
Normal file
363
src/com/android/messaging/util/NotificationPlayer.java
Normal file
@@ -0,0 +1,363 @@
|
||||
/*
|
||||
* 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
src/com/android/messaging/util/OsUtil.java
Normal file
269
src/com/android/messaging/util/OsUtil.java
Normal file
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
40
src/com/android/messaging/util/PendingIntentConstants.java
Normal file
40
src/com/android/messaging/util/PendingIntentConstants.java
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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";
|
||||
}
|
||||
1011
src/com/android/messaging/util/PhoneUtils.java
Normal file
1011
src/com/android/messaging/util/PhoneUtils.java
Normal file
File diff suppressed because it is too large
Load Diff
53
src/com/android/messaging/util/RingtoneUtil.java
Normal file
53
src/com/android/messaging/util/RingtoneUtil.java
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
176
src/com/android/messaging/util/SafeAsyncTask.java
Normal file
176
src/com/android/messaging/util/SafeAsyncTask.java
Normal file
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
129
src/com/android/messaging/util/SwitchCompatUtils.java
Normal file
129
src/com/android/messaging/util/SwitchCompatUtils.java
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
73
src/com/android/messaging/util/TextUtil.java
Normal file
73
src/com/android/messaging/util/TextUtil.java
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
28
src/com/android/messaging/util/ThreadUtil.java
Normal file
28
src/com/android/messaging/util/ThreadUtil.java
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
70
src/com/android/messaging/util/TintDrawableWrapper.java
Normal file
70
src/com/android/messaging/util/TintDrawableWrapper.java
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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
src/com/android/messaging/util/Trace.java
Normal file
115
src/com/android/messaging/util/Trace.java
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/com/android/messaging/util/Typefaces.java
Normal file
45
src/com/android/messaging/util/Typefaces.java
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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
src/com/android/messaging/util/UiUtils.java
Normal file
438
src/com/android/messaging/util/UiUtils.java
Normal file
@@ -0,0 +1,438 @@
|
||||
/*
|
||||
* 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
src/com/android/messaging/util/UriUtil.java
Normal file
393
src/com/android/messaging/util/UriUtil.java
Normal file
@@ -0,0 +1,393 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
67
src/com/android/messaging/util/VersionUtil.java
Normal file
67
src/com/android/messaging/util/VersionUtil.java
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
121
src/com/android/messaging/util/WakeLockHelper.java
Normal file
121
src/com/android/messaging/util/WakeLockHelper.java
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/com/android/messaging/util/YouTubeUtil.java
Normal file
98
src/com/android/messaging/util/YouTubeUtil.java
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
140
src/com/android/messaging/util/exif/CountedDataInputStream.java
Normal file
140
src/com/android/messaging/util/exif/CountedDataInputStream.java
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
349
src/com/android/messaging/util/exif/ExifData.java
Normal file
349
src/com/android/messaging/util/exif/ExifData.java
Normal file
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
2448
src/com/android/messaging/util/exif/ExifInterface.java
Normal file
2448
src/com/android/messaging/util/exif/ExifInterface.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
196
src/com/android/messaging/util/exif/ExifModifier.java
Normal file
196
src/com/android/messaging/util/exif/ExifModifier.java
Normal file
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
522
src/com/android/messaging/util/exif/ExifOutputStream.java
Normal file
522
src/com/android/messaging/util/exif/ExifOutputStream.java
Normal file
@@ -0,0 +1,522 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
918
src/com/android/messaging/util/exif/ExifParser.java
Normal file
918
src/com/android/messaging/util/exif/ExifParser.java
Normal file
@@ -0,0 +1,918 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
93
src/com/android/messaging/util/exif/ExifReader.java
Normal file
93
src/com/android/messaging/util/exif/ExifReader.java
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
1008
src/com/android/messaging/util/exif/ExifTag.java
Normal file
1008
src/com/android/messaging/util/exif/ExifTag.java
Normal file
File diff suppressed because it is too large
Load Diff
152
src/com/android/messaging/util/exif/IfdData.java
Normal file
152
src/com/android/messaging/util/exif/IfdData.java
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
31
src/com/android/messaging/util/exif/IfdId.java
Normal file
31
src/com/android/messaging/util/exif/IfdId.java
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
}
|
||||
39
src/com/android/messaging/util/exif/JpegHeader.java
Normal file
39
src/com/android/messaging/util/exif/JpegHeader.java
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
88
src/com/android/messaging/util/exif/Rational.java
Normal file
88
src/com/android/messaging/util/exif/Rational.java
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user