diff --git a/GmsApi b/GmsApi index 4b5be5f4..cee8188d 160000 --- a/GmsApi +++ b/GmsApi @@ -1 +1 @@ -Subproject commit 4b5be5f4bb6b5e4e88a74011e8c150594769bcba +Subproject commit cee8188daef20716b4879be8f34c3ad730161e17 diff --git a/src/org/microg/gms/auth/AskPermissionActivity.java b/src/org/microg/gms/auth/AskPermissionActivity.java index c3dcc4c4..44b13da0 100644 --- a/src/org/microg/gms/auth/AskPermissionActivity.java +++ b/src/org/microg/gms/auth/AskPermissionActivity.java @@ -38,7 +38,7 @@ import android.widget.TextView; import com.google.android.gms.R; import org.microg.gms.common.PackageUtils; -import org.microg.gms.userinfo.ProfileManager; +import org.microg.gms.people.PeopleManager; import java.io.IOException; @@ -83,7 +83,7 @@ public class AskPermissionActivity extends AccountAuthenticatorActivity { } CharSequence appLabel = packageManager.getApplicationLabel(applicationInfo); Drawable appIcon = packageManager.getApplicationIcon(applicationInfo); - Bitmap profileIcon = ProfileManager.getProfilePicture(this, account, false); + Bitmap profileIcon = PeopleManager.getUserPicture(this, account, false); // receive profile icon if (profileIcon != null) { @@ -92,7 +92,7 @@ public class AskPermissionActivity extends AccountAuthenticatorActivity { new Thread(new Runnable() { @Override public void run() { - final Bitmap profileIcon = ProfileManager.getProfilePicture(AskPermissionActivity.this, account, true); + final Bitmap profileIcon = PeopleManager.getUserPicture(AskPermissionActivity.this, account, true); runOnUiThread(new Runnable() { @Override public void run() { diff --git a/src/org/microg/gms/auth/login/LoginActivity.java b/src/org/microg/gms/auth/login/LoginActivity.java index 92ed3d3a..50884263 100644 --- a/src/org/microg/gms/auth/login/LoginActivity.java +++ b/src/org/microg/gms/auth/login/LoginActivity.java @@ -43,7 +43,7 @@ import org.microg.gms.auth.AuthResponse; import org.microg.gms.common.Constants; import org.microg.gms.common.HttpFormClient; import org.microg.gms.common.Utils; -import org.microg.gms.userinfo.ProfileManager; +import org.microg.gms.people.PeopleManager; import java.util.Locale; @@ -208,25 +208,7 @@ public class LoginActivity extends AssistantActivity { AuthManager.storeResponse(LoginActivity.this, account, Constants.GMS_PACKAGE_NAME, Constants.GMS_PACKAGE_SIGNATURE_SHA1, service, response); - retrieveGmsKeyUserinfoProfile(account); - } - - @Override - public void onException(Exception exception) { - Log.w(TAG, "onException: " + exception); - } - }); - } - - private void retrieveGmsKeyUserinfoProfile(final Account account) { - ProfileManager.getAuthKeyRequest(this, account) - .getResponseAsync(new HttpFormClient.Callback() { - @Override - public void onResponse(AuthResponse response) { - AuthManager.storeResponse(LoginActivity.this, account, - Constants.GMS_PACKAGE_NAME, Constants.GMS_PACKAGE_SIGNATURE_SHA1, - ProfileManager.SERVICE_TOKEN, response); - ProfileManager.storeAuthKey(LoginActivity.this, account, response.auth); + PeopleManager.loadUserInfo(LoginActivity.this, account); finish(); } diff --git a/src/org/microg/gms/checkin/CheckinService.java b/src/org/microg/gms/checkin/CheckinService.java index 22916de3..1c7f8b2b 100644 --- a/src/org/microg/gms/checkin/CheckinService.java +++ b/src/org/microg/gms/checkin/CheckinService.java @@ -16,10 +16,14 @@ package org.microg.gms.checkin; +import android.accounts.Account; +import android.accounts.AccountManager; import android.app.IntentService; import android.content.Intent; import android.util.Log; +import org.microg.gms.people.PeopleManager; + public class CheckinService extends IntentService { private static final String TAG = "GmsCheckinSvc"; @@ -34,6 +38,9 @@ public class CheckinService extends IntentService { if (info != null) { Log.d(TAG, "Checked in as " + Long.toHexString(info.androidId)); } + for (Account account : AccountManager.get(this).getAccountsByType("com.google")) { + PeopleManager.loadUserInfo(this, account); + } } catch (Exception e) { Log.w(TAG, e); } diff --git a/src/org/microg/gms/people/DatabaseHelper.java b/src/org/microg/gms/people/DatabaseHelper.java index fc4c3522..6bda73e0 100644 --- a/src/org/microg/gms/people/DatabaseHelper.java +++ b/src/org/microg/gms/people/DatabaseHelper.java @@ -16,6 +16,7 @@ package org.microg.gms.people; +import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; @@ -26,26 +27,28 @@ public class DatabaseHelper extends SQLiteOpenHelper { private static final String DB_NAME = "pluscontacts.db"; private static final String CREATE_OWNERS = "CREATE TABLE owners (" + "_id INTEGER PRIMARY KEY AUTOINCREMENT," + - "account_name TEXT NOT NULL UNIQUE," + - "gaia_id TEXT," + + "account_name TEXT NOT NULL UNIQUE," + // example@gmail.com + "gaia_id TEXT," + // 123456789123456789123 "page_gaia_id TEXT," + - "display_name TEXT," + - "avatar TEXT," + - "cover_photo_url TEXT," + + "display_name TEXT," + // firstName lastName + "avatar TEXT," + // url (relative?) + "cover_photo_url TEXT," + // cover url (relative?) "cover_photo_height INTEGER NOT NULL DEFAULT 0," + "cover_photo_width INTEGER NOT NULL DEFAULT 0," + "cover_photo_id TEXT," + - "last_sync_start_time INTEGER NOT NULL DEFAULT 0," + - "last_sync_finish_time INTEGER NOT NULL DEFAULT 0," + - "last_sync_status INTEGER NOT NULL DEFAULT 0," + - "last_successful_sync_time INTEGER NOT NULL DEFAULT 0," + - "sync_to_contacts INTEGER NOT NULL DEFAULT 0," + - "is_dasher INTEGER NOT NULL DEFAULT 0," + + "last_sync_start_time INTEGER NOT NULL DEFAULT 0," + // timestamp + "last_sync_finish_time INTEGER NOT NULL DEFAULT 0," + // timestamp + "last_sync_status INTEGER NOT NULL DEFAULT 0," + // eg. 2 + "last_successful_sync_time INTEGER NOT NULL DEFAULT 0," + // timestamp + "sync_to_contacts INTEGER NOT NULL DEFAULT 0," + // 0 + "is_dasher INTEGER NOT NULL DEFAULT 0," + // 0 "dasher_domain TEXT," + "etag TEXT," + - "sync_circles_to_contacts INTEGER NOT NULL DEFAULT 0," + - "sync_evergreen_to_contacts INTEGER NOT NULL DEFAULT 0," + - "last_full_people_sync_time INTEGER NOT NULL DEFAULT 0);"; + "sync_circles_to_contacts INTEGER NOT NULL DEFAULT 0," + // 0 + "sync_evergreen_to_contacts INTEGER NOT NULL DEFAULT 0," + // 0 + "last_full_people_sync_time INTEGER NOT NULL DEFAULT 0);"; // timestamp + public static final String OWNERS_TABLE = "owners"; + public DatabaseHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); @@ -67,6 +70,14 @@ public class DatabaseHelper extends SQLiteOpenHelper { } public Cursor getOwners() { - return getReadableDatabase().query("owners", null, null, null, null, null, null); + return getReadableDatabase().query(OWNERS_TABLE, null, null, null, null, null, null); + } + + public void putOwner(ContentValues contentValues) { + getWritableDatabase().insertWithOnConflict(OWNERS_TABLE, null, contentValues, SQLiteDatabase.CONFLICT_REPLACE); + } + + public Cursor getOwner(String accountName) { + return getReadableDatabase().query(OWNERS_TABLE, null, "account_name=?", new String[]{accountName}, null, null, null); } } diff --git a/src/org/microg/gms/people/PeopleManager.java b/src/org/microg/gms/people/PeopleManager.java new file mode 100644 index 00000000..a0c5fd6b --- /dev/null +++ b/src/org/microg/gms/people/PeopleManager.java @@ -0,0 +1,121 @@ +/* + * Copyright 2013-2015 µg Project Team + * + * 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 org.microg.gms.people; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; +import org.microg.gms.auth.AuthManager; +import org.microg.gms.auth.AuthRequest; +import org.microg.gms.auth.AuthResponse; +import org.microg.gms.common.Constants; +import org.microg.gms.common.Utils; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; + +public class PeopleManager { + private static final String TAG = "GmsPeopleManager"; + public static final String USERINFO_SCOPE = "oauth2:https://www.googleapis.com/auth/userinfo.profile"; + public static final String USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo"; + public static final String REGEX_SEARCH_USER_PHOTO = "https?\\:\\/\\/lh([0-9]*)\\.googleusercontent\\.com/"; + + public static Bitmap getUserPicture(Context context, Account account, boolean network) { + DatabaseHelper databaseHelper = new DatabaseHelper(context); + Cursor cursor = databaseHelper.getOwner(account.name); + String url = null; + if (cursor.moveToNext()) { + int idx = cursor.getColumnIndex("avatar"); + if (!cursor.isNull(idx)) url = cursor.getString(idx); + } + cursor.close(); + databaseHelper.close(); + if (url == null) return null; + // TODO: load from cache + if (!network) return null; + url = "https://lh" + url.toCharArray()[1] + ".googleusercontent.com" + url.substring(2); + try { + URLConnection conn = new URL(url).openConnection(); + conn.setDoInput(true); + byte[] bytes = Utils.readStreamToEnd(conn.getInputStream()); + // TODO: store to cache + Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + return bitmap; + } catch (Exception e) { + Log.w(TAG, e); + return null; + } + } + + public static void loadUserInfo(Context context, Account account) { + try { + URLConnection conn = new URL(USERINFO_URL).openConnection(); + conn.addRequestProperty("Authorization", "Bearer " + getUserInfoAuthKey(context, account)); + conn.setDoInput(true); + byte[] bytes = Utils.readStreamToEnd(conn.getInputStream()); + JSONObject info = new JSONObject(new String(bytes)); + ContentValues contentValues = new ContentValues(); + contentValues.put("account_name", account.name); + if (info.has("id")) contentValues.put("gaia_id", info.getString("id")); + if (info.has("picture")) + contentValues.put("avatar", info.getString("picture").replaceFirst(REGEX_SEARCH_USER_PHOTO, "~$1/")); + if (info.has("name")) contentValues.put("display_name", info.getString("name")); + DatabaseHelper databaseHelper = new DatabaseHelper(context); + databaseHelper.putOwner(contentValues); + databaseHelper.close(); + } catch (JSONException | IOException e) { + Log.w(TAG, e); + } + } + + public static AuthRequest getUserInfoAuthKeyRequest(Context context, Account account) { + return new AuthRequest().fromContext(context) + .appIsGms().callerIsGms() + .service(USERINFO_SCOPE) + .email(account.name) + .token(AccountManager.get(context).getPassword(account)) + .systemPartition() + .hasPermission() + .getAccountId(); + } + + public static String getUserInfoAuthKey(Context context, Account account) { + String result = AuthManager.getToken(context, account, Constants.GMS_PACKAGE_NAME, Constants.GMS_PACKAGE_SIGNATURE_SHA1, + USERINFO_SCOPE); + if (result == null) { + try { + AuthResponse response = getUserInfoAuthKeyRequest(context, account).getResponse(); + AuthManager.storeResponse(context, account, + Constants.GMS_PACKAGE_NAME, Constants.GMS_PACKAGE_SIGNATURE_SHA1, + USERINFO_SCOPE, response); + result = response.auth; + } catch (IOException e) { + return null; + } + } + return result; + } +} diff --git a/src/org/microg/gms/people/PeopleServiceImpl.java b/src/org/microg/gms/people/PeopleServiceImpl.java index 7ff981c6..ce2a07ac 100644 --- a/src/org/microg/gms/people/PeopleServiceImpl.java +++ b/src/org/microg/gms/people/PeopleServiceImpl.java @@ -25,6 +25,7 @@ import android.os.RemoteException; import android.util.Log; import com.google.android.gms.common.data.DataHolder; +import com.google.android.gms.common.internal.ICancelToken; import com.google.android.gms.people.internal.IPeopleCallbacks; import com.google.android.gms.people.internal.IPeopleService; @@ -39,26 +40,34 @@ public class PeopleServiceImpl extends IPeopleService.Stub { @Override public void loadOwners(final IPeopleCallbacks callbacks, boolean var2, boolean var3, final String accountName, String var5, int sortOrder) { Log.d(TAG, "loadOwners: " + var2 + ", " + var3 + ", " + accountName + ", " + var5 + ", " + sortOrder); - new Thread(new Runnable() { - @Override - public void run() { - AccountManager accountManager = AccountManager.get(context); - Bundle result = new Bundle(); - for (Account account : accountManager.getAccountsByType("com.google")) { - if (accountName == null || account.name.equals(accountName)) { - result.putParcelable(account.name, null); - } - } - try { - DatabaseHelper databaseHelper = new DatabaseHelper(context); - DataHolder dataHolder = DataHolder.fromCursor(databaseHelper.getOwners(), 0, result); - Log.d(TAG, "loadOwners[result]: " + dataHolder); - callbacks.onDataHolder(0, result, dataHolder); - } catch (Exception e) { - Log.w(TAG, e); - } + AccountManager accountManager = AccountManager.get(context); + Bundle result = new Bundle(); + for (Account account : accountManager.getAccountsByType("com.google")) { + if (accountName == null || account.name.equals(accountName)) { + result.putParcelable(account.name, null); } - }).start(); + } + try { + DatabaseHelper databaseHelper = new DatabaseHelper(context); + DataHolder dataHolder = DataHolder.fromCursor(databaseHelper.getOwners(), 0, result); + Log.d(TAG, "loadOwners[result]: " + dataHolder); + callbacks.onDataHolder(0, result, dataHolder); + databaseHelper.close(); + } catch (Exception e) { + Log.w(TAG, e); + } + } + + @Override + public Bundle registerDataChangedListener(IPeopleCallbacks callbacks, boolean register, String var3, String var4, int scopes) { + Log.d(TAG, "registerDataChangedListener: " + register + ", " + var3 + ", " + var4 + ", " + scopes); + return null; + } + + @Override + public ICancelToken loadOwnerAvatar(IPeopleCallbacks callbacks, String account, String pageId, int size, int flags) { + Log.d(TAG, "loadOwnerAvatar: " + account + ", " + pageId + ", " + size + ", " + flags); + return null; } @Override diff --git a/src/org/microg/gms/userinfo/ProfileManager.java b/src/org/microg/gms/userinfo/ProfileManager.java deleted file mode 100644 index 14e96d00..00000000 --- a/src/org/microg/gms/userinfo/ProfileManager.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2013-2015 µg Project Team - * - * 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 org.microg.gms.userinfo; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.util.Base64; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; -import org.microg.gms.auth.AuthManager; -import org.microg.gms.auth.AuthRequest; -import org.microg.gms.auth.AuthResponse; -import org.microg.gms.common.Constants; -import org.microg.gms.common.Utils; - -import java.io.IOException; -import java.net.URL; -import java.net.URLConnection; - -public class ProfileManager { - private static final String TAG = "GmsProfileManager"; - private static final String PREFERENCES_NAME = "profile_manager"; - public static final String SERVICE_TOKEN = "oauth2:https://www.googleapis.com/auth/userinfo.profile"; - - public static class ProfileInfo { - public final String familyName; - public final String givenName; - public final long id; - public final String link; - public final String locale; - public final String name; - public final String picture; - - public ProfileInfo(String familyName, String givenName, long id, String link, String locale, String name, String picture) { - this.familyName = familyName; - this.givenName = givenName; - this.id = id; - this.link = link; - this.locale = locale; - this.name = name; - this.picture = picture; - } - - public static ProfileInfo parse(byte[] bytes) throws JSONException { - return parse(new String(bytes)); - } - - private static ProfileInfo parse(String info) throws JSONException { - return parse(new JSONObject(info)); - } - - private static ProfileInfo parse(JSONObject info) throws JSONException { - return new ProfileInfo( - info.has("family_name") ? info.getString("family_name") : null, - info.has("given_name") ? info.getString("given_name") : null, - info.has("id") ? info.getLong("id") : 0, - info.has("link") ? info.getString("link") : null, - info.has("locale") ? info.getString("locale") : null, - info.has("name") ? info.getString("name") : null, - info.has("picture") ? info.getString("picture") : null); - } - } - - public static SharedPreferences getPreferences(Context context) { - return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); - } - - public static Bitmap getProfilePicture(Context context, Account account, boolean network) { - SharedPreferences preferences = getPreferences(context); - String picture = preferences.getString("profile_picture", null); - if (picture != null) { - byte[] bytes = Base64.decode(picture, Base64.DEFAULT); - return BitmapFactory.decodeByteArray(bytes, 0, bytes.length); - } - if (!network) return null; - try { - URLConnection conn = new URL(getProfileInfo(context, account).picture).openConnection(); - conn.setDoInput(true); - byte[] bytes = Utils.readStreamToEnd(conn.getInputStream()); - Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); - if (bitmap != null) - preferences.edit().putString("profile_picture", Base64.encodeToString(bytes, Base64.DEFAULT)).apply(); - return bitmap; - } catch (Exception e) { - Log.w(TAG, e); - return null; - } - } - - public static ProfileInfo getProfileInfo(Context context, Account account) { - try { - URLConnection conn = new URL("https://www.googleapis.com/oauth2/v1/userinfo").openConnection(); - conn.addRequestProperty("Authorization", "Bearer " + getAuthKey(context, account)); - conn.setDoInput(true); - byte[] bytes = Utils.readStreamToEnd(conn.getInputStream()); - return ProfileInfo.parse(bytes); - } catch (JSONException | IOException e) { - Log.w(TAG, e); - return null; - } - } - - public static AuthRequest getAuthKeyRequest(Context context, Account account) { - return new AuthRequest().fromContext(context) - .appIsGms().callerIsGms() - .service(SERVICE_TOKEN) - .email(account.name) - .token(AccountManager.get(context).getPassword(account)) - .systemPartition() - .hasPermission() - .getAccountId(); - } - - public static String getAuthKey(Context context, Account account) { - String result = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) - .getString(account.name + "_auth_key", null); - if (result == null) { - try { - AuthResponse response = getAuthKeyRequest(context, account).getResponse(); - AuthManager.storeResponse(context, account, - Constants.GMS_PACKAGE_NAME, Constants.GMS_PACKAGE_SIGNATURE_SHA1, - SERVICE_TOKEN, response); - result = response.auth; - storeAuthKey(context, account, result); - } catch (IOException e) { - return null; - } - } - return result; - } - - public static void storeAuthKey(Context context, Account account, String key) { - context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE).edit() - .putString(account.name + "_auth_key", key).commit(); - } -}