1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.accounts;
18 
19 import android.accounts.Account;
20 import android.app.settings.SettingsEnums;
21 import android.content.ContentResolver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.pm.PackageManager;
25 import android.content.pm.ResolveInfo;
26 import android.graphics.Bitmap;
27 import android.net.Uri;
28 import android.os.Bundle;
29 import android.text.TextUtils;
30 import android.util.Log;
31 import android.widget.ImageView;
32 
33 import androidx.annotation.VisibleForTesting;
34 import androidx.lifecycle.Lifecycle;
35 import androidx.lifecycle.LifecycleObserver;
36 import androidx.lifecycle.MutableLiveData;
37 import androidx.lifecycle.OnLifecycleEvent;
38 
39 import com.android.settings.R;
40 import com.android.settings.activityembedding.ActivityEmbeddingRulesController;
41 import com.android.settings.homepage.SettingsHomepageActivity;
42 import com.android.settings.overlay.FeatureFactory;
43 import com.android.settingslib.utils.ThreadUtils;
44 
45 import java.net.URISyntaxException;
46 import java.util.List;
47 
48 /**
49  * Avatar related work to the onStart method of registered observable classes
50  * in {@link SettingsHomepageActivity}.
51  */
52 public class AvatarViewMixin implements LifecycleObserver {
53     private static final String TAG = "AvatarViewMixin";
54 
55     @VisibleForTesting
56     static final Intent INTENT_GET_ACCOUNT_DATA =
57             new Intent("android.content.action.SETTINGS_ACCOUNT_DATA");
58 
59     private static final String METHOD_GET_ACCOUNT_AVATAR = "getAccountAvatar";
60     private static final String KEY_AVATAR_BITMAP = "account_avatar";
61     private static final String KEY_ACCOUNT_NAME = "account_name";
62     private static final String KEY_AVATAR_ICON = "avatar_icon";
63     private static final String EXTRA_ACCOUNT_NAME = "extra.accountName";
64 
65     private final Context mContext;
66     private final ImageView mAvatarView;
67     private final MutableLiveData<Bitmap> mAvatarImage;
68 
69     @VisibleForTesting
70     String mAccountName;
71 
72     /**
73      * @return true if the avatar icon is supported.
74      */
isAvatarSupported(Context context)75     public static boolean isAvatarSupported(Context context) {
76         if (!context.getResources().getBoolean(R.bool.config_show_avatar_in_homepage)) {
77             Log.d(TAG, "Feature disabled by config. Skipping");
78             return false;
79         }
80         return true;
81     }
82 
AvatarViewMixin(SettingsHomepageActivity activity, ImageView avatarView)83     public AvatarViewMixin(SettingsHomepageActivity activity, ImageView avatarView) {
84         mContext = activity.getApplicationContext();
85         mAvatarView = avatarView;
86         mAvatarView.setOnClickListener(v -> {
87             Intent intent;
88             try {
89                 final String uri = mContext.getResources().getString(
90                         R.string.config_account_intent_uri);
91                 intent = Intent.parseUri(uri, Intent.URI_INTENT_SCHEME);
92             } catch (URISyntaxException e) {
93                 Log.w(TAG, "Error parsing avatar mixin intent, skipping", e);
94                 return;
95             }
96 
97             if (!TextUtils.isEmpty(mAccountName)) {
98                 intent.putExtra(EXTRA_ACCOUNT_NAME, mAccountName);
99             }
100 
101             final List<ResolveInfo> matchedIntents =
102                     mContext.getPackageManager().queryIntentActivities(intent,
103                             PackageManager.MATCH_SYSTEM_ONLY);
104             if (matchedIntents.isEmpty()) {
105                 Log.w(TAG, "Cannot find any matching action VIEW_ACCOUNT intent.");
106                 return;
107             }
108 
109             // Set a component name since activity embedding requires a component name for
110             // registering a rule.
111             intent.setComponent(matchedIntents.get(0).getComponentInfo().getComponentName());
112             ActivityEmbeddingRulesController.registerTwoPanePairRuleForSettingsHome(
113                     mContext,
114                     intent.getComponent(),
115                     intent.getAction(),
116                     false /* finishPrimaryWithSecondary */,
117                     true /* finishSecondaryWithPrimary */,
118                     false /* clearTop */);
119 
120             FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
121                     .logSettingsTileClick(KEY_AVATAR_ICON, SettingsEnums.SETTINGS_HOMEPAGE);
122 
123             // Here may have two different UI while start the activity.
124             // It will display adding account UI when device has no any account.
125             // It will display account information page when intent added the specified account.
126             activity.startActivity(intent);
127         });
128 
129         mAvatarImage = new MutableLiveData<>();
130         mAvatarImage.observe(activity, bitmap -> {
131             avatarView.setImageBitmap(bitmap);
132         });
133     }
134 
135     @OnLifecycleEvent(Lifecycle.Event.ON_START)
onStart()136     public void onStart() {
137         if (hasAccount()) {
138             loadAccount();
139         } else {
140             mAccountName = null;
141             mAvatarView.setImageResource(R.drawable.ic_account_circle_24dp);
142         }
143     }
144 
145     @VisibleForTesting
hasAccount()146     boolean hasAccount() {
147         final Account[] accounts = FeatureFactory.getFeatureFactory().getAccountFeatureProvider()
148                 .getAccounts(mContext);
149         return (accounts != null) && (accounts.length > 0);
150     }
151 
loadAccount()152     private void loadAccount() {
153         final String authority = queryProviderAuthority();
154         if (TextUtils.isEmpty(authority)) {
155             return;
156         }
157 
158         ThreadUtils.postOnBackgroundThread(() -> {
159             final Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
160                     .authority(authority)
161                     .build();
162             final Bundle bundle = mContext.getContentResolver().call(uri,
163                     METHOD_GET_ACCOUNT_AVATAR, null /* arg */, null /* extras */);
164             final Bitmap bitmap = bundle.getParcelable(KEY_AVATAR_BITMAP);
165             mAccountName = bundle.getString(KEY_ACCOUNT_NAME, "" /* defaultValue */);
166             mAvatarImage.postValue(bitmap);
167         });
168     }
169 
170     @VisibleForTesting
queryProviderAuthority()171     String queryProviderAuthority() {
172         final List<ResolveInfo> providers =
173                 mContext.getPackageManager().queryIntentContentProviders(INTENT_GET_ACCOUNT_DATA,
174                         PackageManager.MATCH_SYSTEM_ONLY);
175         if (providers.size() == 1) {
176             return providers.get(0).providerInfo.authority;
177         } else {
178             Log.w(TAG, "The size of the provider is " + providers.size());
179             return null;
180         }
181     }
182 }
183