/*
* Copyright 2015. Appsi Mobile
*
* 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.appsimobile.appsii.module.home;
import android.content.AsyncTaskLoader;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.ContactsContract;
import android.text.TextUtils;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
/**
* A loader that can load very basic contact info.
*/
class RawContactsLoader extends AsyncTaskLoader<Contact> {
static final Set<Long> sNotifiedRawContactIds = new HashSet<>();
Contact mContact;
ForceLoadContentObserver mObserver;
final long mId;
final String mLookupKey;
public RawContactsLoader(Context context, String lookupKey, long id) {
super(context);
mId = id;
mLookupKey = lookupKey;
}
public static Contact loadContact(Context context, long id, String lookupKey) {
if (lookupKey == null) return null;
Uri uri = ContactsContract.Contacts.getLookupUri(id, lookupKey);
Cursor c = context.getContentResolver().query(
uri,
new String[]{
ContactsContract.Contacts._ID,
ContactsContract.Contacts.PHOTO_FILE_ID,
ContactsContract.Contacts.PHOTO_URI,
ContactsContract.Contacts.LOOKUP_KEY,
},
null,
null,
null);
if (c != null && c.moveToNext()) {
Contact contact = new Contact();
contact.mId = c.getLong(0);
contact.mPhotoId = c.getLong(1);
contact.mPhotoUri = c.getString(2);
contact.mLookupKey = c.getString(3);
loadPhotoBinaryData(context, contact);
c.close();
loadRawContacts(context, contact);
postViewNotificationToSyncAdapter(context, contact);
return contact;
}
return null;
}
/**
* Looks for the photo data item in entities. If found, creates a new Bitmap instance. If
* not found, returns null
*/
private static void loadPhotoBinaryData(Context context, Contact contactData) {
// If we have a photo URI, try loading that first.
String photoUri = contactData.mPhotoUri;
if (photoUri != null) {
try {
final InputStream inputStream;
final AssetFileDescriptor fd;
final Uri uri = Uri.parse(photoUri);
final String scheme = uri.getScheme();
if ("http".equals(scheme) || "https".equals(scheme)) {
// Support HTTP urls that might come from extended directories
inputStream = new URL(photoUri).openStream();
fd = null;
} else {
fd = context.getContentResolver().openAssetFileDescriptor(uri, "r");
inputStream = fd.createInputStream();
}
byte[] buffer = new byte[16 * 1024];
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
int size;
while ((size = inputStream.read(buffer)) != -1) {
baos.write(buffer, 0, size);
}
byte[] data = baos.toByteArray();
contactData.mBitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
} finally {
inputStream.close();
if (fd != null) {
fd.close();
}
}
return;
} catch (IOException ioe) {
// Just fall back to the case below.
}
}
// If we couldn't load from a file, fall back to the data blob.
final long photoId = contactData.mPhotoId;
if (photoId <= 0) {
// No photo ID
return;
}
try {
AssetFileDescriptor assetFileDescriptor = openDisplayPhoto(context, photoId);
if (assetFileDescriptor != null) {
InputStream in = assetFileDescriptor.createInputStream();
try {
contactData.mBitmap = BitmapFactory.decodeStream(in);
} finally {
in.close();
assetFileDescriptor.close();
}
}
} catch (IOException e) {
Log.wtf("ProfileImage", "Error loading image", e);
}
}
private static void loadRawContacts(Context context, Contact contact) {
Cursor c = context.getContentResolver().query(
ContactsContract.RawContacts.CONTENT_URI,
new String[]{
ContactsContract.RawContacts.ACCOUNT_TYPE,
ContactsContract.RawContacts._ID,
ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY,
},
ContactsContract.RawContacts.CONTACT_ID + "=?",
new String[]{String.valueOf(contact.mId)},
null);
while (c.moveToNext()) {
RawContact rawContact = new RawContact();
rawContact.mAccountType = c.getString(0);
rawContact.mId = c.getLong(1);
contact.mRawContacts.add(rawContact);
}
c.close();
}
/**
* Posts a message to the contributing sync adapters that have opted-in, notifying them
* that the contact has just been loaded
*/
private static void postViewNotificationToSyncAdapter(Context context, Contact contact) {
ArrayList<RawContact> mRawContacts = contact.mRawContacts;
int N = mRawContacts.size();
for (int i = 0; i < N; i++) {
RawContact rawContact = mRawContacts.get(i);
final long rawContactId = rawContact.mId;
if (sNotifiedRawContactIds.contains(rawContactId)) {
continue; // Already notified for this raw contact.
}
sNotifiedRawContactIds.add(rawContactId);
if (!rawContact.isGoogleAccount()) continue;
final String serviceName = contact.getViewContactNotifyServiceClassName();
final String servicePackageName = contact.getViewContactNotifyServicePackageName();
final String serviceName2 = contact.getViewContactNotifyServiceClassName2();
final String servicePackageName2 = contact.getViewContactNotifyServicePackageName2();
sendViewIntent(context, rawContactId, serviceName, servicePackageName);
sendViewIntent(context, rawContactId, serviceName2, servicePackageName2);
}
}
public static AssetFileDescriptor openDisplayPhoto(Context context, long photoFileId) {
Uri displayPhotoUri =
ContentUris.withAppendedId(ContactsContract.DisplayPhoto.CONTENT_URI, photoFileId);
try {
return context.getContentResolver().openAssetFileDescriptor(
displayPhotoUri, "r");
} catch (IOException e) {
return null;
}
}
private static void sendViewIntent(Context context, long rawContactId, String serviceName,
String servicePackageName) {
if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) {
final Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
rawContactId);
final Intent intent = new Intent();
intent.setClassName(servicePackageName, serviceName);
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(uri, ContactsContract.RawContacts.CONTENT_ITEM_TYPE);
try {
context.startService(intent);
} catch (Exception e) {
Log.e("ProfileImageFragment", "Error sending message to source-app", e);
}
}
}
/**
* Handles a request to cancel a load.
*/
@Override
public void onCanceled(Contact apps) {
super.onCanceled(apps);
// At this point we can release the resources associated with 'apps'
// if needed.
onReleaseResources(apps);
}
/**
* This is where the bulk of our work is done. This function is
* called in a background thread and should generate a new set of
* data to be published by the loader.
*/
@Override
public Contact loadInBackground() {
return loadContact(getContext(), mId, mLookupKey);
}
/**
* Helper function to take care of releasing resources associated
* with an actively loaded data set.
*/
protected void onReleaseResources(Contact apps) {
// For a simple List<> there is nothing to do. For something
// like a Cursor, we would close it here.
}
/**
* Called when there is new data to deliver to the client. The
* super class will take care of delivering it; the implementation
* here just adds a little more logic.
*/
@Override
public void deliverResult(Contact apps) {
if (isReset()) {
// An async query came in while the loader is stopped. We
// don't need the result.
if (apps != null) {
onReleaseResources(apps);
}
}
Contact oldApps = mContact;
mContact = apps;
if (isStarted()) {
// If the Loader is currently started, we can immediately
// deliver its results.
super.deliverResult(apps);
}
// At this point we can release the resources associated with
// 'oldApps' if needed; now that the new result is delivered we
// know that it is no longer in use.
if (oldApps != null) {
onReleaseResources(oldApps);
}
}
/**
* Handles a request to start the Loader.
*/
@Override
protected void onStartLoading() {
if (mContact != null) {
// If we currently have a result available, deliver it
// immediately.
deliverResult(mContact);
}
// Start watching for changes in the app data.
if (mObserver == null) {
mObserver = new ForceLoadContentObserver();
Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, mId);
getContext().getContentResolver().registerContentObserver(uri, true, mObserver);
}
if (takeContentChanged() || mContact == null) {
// If the data has changed since the last time it was loaded
// or is not currently available, start a load.
forceLoad();
}
}
/**
* Handles a request to stop the Loader.
*/
@Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
/**
* Handles a request to completely reset the Loader.
*/
@Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
// At this point we can release the resources associated with 'apps'
// if needed.
if (mContact != null) {
onReleaseResources(mContact);
mContact = null;
}
// Stop monitoring for changes.
if (mObserver != null) {
getContext().getContentResolver().unregisterContentObserver(mObserver);
mObserver = null;
}
}
}