1package eu.siacs.conversations.ui;
2
3import android.Manifest;
4import android.content.ActivityNotFoundException;
5import android.content.Intent;
6import android.content.pm.ActivityInfo;
7import android.net.Uri;
8import android.os.Bundle;
9import android.security.KeyChain;
10import android.security.KeyChainAliasCallback;
11import android.util.Log;
12import android.view.Menu;
13import android.view.MenuItem;
14import android.view.View;
15import android.view.ViewGroup;
16import android.widget.Toast;
17
18import androidx.annotation.NonNull;
19import androidx.appcompat.app.AppCompatActivity;
20import androidx.databinding.DataBindingUtil;
21import androidx.viewpager.widget.PagerAdapter;
22import androidx.viewpager.widget.ViewPager;
23
24import java.security.SecureRandom;
25import java.util.Arrays;
26import java.util.List;
27import java.util.HashSet;
28import java.util.UUID;
29
30import eu.siacs.conversations.Config;
31import eu.siacs.conversations.R;
32import eu.siacs.conversations.databinding.ActivityWelcomeBinding;
33import eu.siacs.conversations.entities.Account;
34import eu.siacs.conversations.services.XmppConnectionService;
35import eu.siacs.conversations.utils.CryptoHelper;
36import eu.siacs.conversations.utils.Compatibility;
37import eu.siacs.conversations.utils.InstallReferrerUtils;
38import eu.siacs.conversations.utils.SignupUtils;
39import eu.siacs.conversations.utils.XmppUri;
40import eu.siacs.conversations.xmpp.Jid;
41
42import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
43import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
44
45public class WelcomeActivity extends XmppActivity implements XmppConnectionService.OnAccountCreated, XmppConnectionService.OnAccountUpdate, KeyChainAliasCallback {
46
47 private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
48
49 private XmppUri inviteUri;
50 private Account onboardingAccount = null;
51
52 public static void launch(AppCompatActivity activity) {
53 Intent intent = new Intent(activity, WelcomeActivity.class);
54 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
55 activity.startActivity(intent);
56 activity.overridePendingTransition(0, 0);
57 }
58
59 public void onInstallReferrerDiscovered(final Uri referrer) {
60 Log.d(Config.LOGTAG, "welcome activity: on install referrer discovered " + referrer);
61 if ("xmpp".equalsIgnoreCase(referrer.getScheme())) {
62 final XmppUri xmppUri = new XmppUri(referrer);
63 runOnUiThread(() -> processXmppUri(xmppUri));
64 } else {
65 Log.i(Config.LOGTAG, "install referrer was not an XMPP uri");
66 }
67 }
68
69 private void processXmppUri(final XmppUri xmppUri) {
70 if (!xmppUri.isValidJid()) {
71 return;
72 }
73 final String preAuth = xmppUri.getParameter(XmppUri.PARAMETER_PRE_AUTH);
74 final Jid jid = xmppUri.getJid();
75 final Intent intent;
76 if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
77 intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth);
78 } else if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) {
79 intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth);
80 intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
81 } else {
82 intent = null;
83 }
84 if (intent != null) {
85 startActivity(intent);
86 finish();
87 return;
88 }
89 this.inviteUri = xmppUri;
90 }
91
92 @Override
93 protected synchronized void refreshUiReal() {
94 if (onboardingAccount == null) return;
95 if (onboardingAccount.getStatus() != Account.State.ONLINE) return;
96
97 Intent intent = new Intent(this, StartConversationActivity.class);
98 intent.putExtra("init", true);
99 intent.putExtra(EXTRA_ACCOUNT, onboardingAccount.getJid().asBareJid().toEscapedString());
100 onboardingAccount = null;
101 startActivity(intent);
102 finish();
103 }
104
105 @Override
106 public void onAccountUpdate() {
107 refreshUi();
108 }
109
110 @Override
111 void onBackendConnected() {
112
113 }
114
115 @Override
116 public void onStart() {
117 super.onStart();
118 final int theme = findTheme();
119 if (this.mTheme != theme) {
120 recreate();
121 }
122 new InstallReferrerUtils(this);
123 }
124
125 @Override
126 public void onStop() {
127 super.onStop();
128 }
129
130 @Override
131 public void onNewIntent(Intent intent) {
132 if (intent != null) {
133 setIntent(intent);
134 }
135 }
136
137 @Override
138 protected void onCreate(final Bundle savedInstanceState) {
139 if (getResources().getBoolean(R.bool.portrait_only)) {
140 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
141 }
142 super.onCreate(savedInstanceState);
143 getPreferences().edit().putStringSet("pstn_gateways", new HashSet<>()).apply();
144 ActivityWelcomeBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_welcome);
145 binding.slideshowPager.setAdapter(new WelcomePagerAdapter(binding.slideshowPager));
146 binding.dotsIndicator.setViewPager(binding.slideshowPager);
147 binding.slideshowPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
148 public void onPageScrollStateChanged(int state) { }
149 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { }
150
151 public void onPageSelected(int position) {
152 binding.buttonNext.setVisibility(position > 1 ? View.GONE : View.VISIBLE);
153 binding.buttonPrivacy.setVisibility(position < 2 ? View.GONE : View.VISIBLE);
154 }
155 });
156 binding.buttonNext.setOnClickListener((v) ->
157 binding.slideshowPager.setCurrentItem(binding.slideshowPager.getCurrentItem() + 1)
158 );
159 binding.buttonPrivacy.setOnClickListener((v) ->
160 startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://cheogram.com/android-privacy.html")))
161 );
162 setSupportActionBar(binding.toolbar);
163 configureActionBar(getSupportActionBar(), false);
164 binding.registerNewAccount.setOnClickListener(v -> {
165 if (hasInviteUri()) {
166 final Intent intent = new Intent(this, MagicCreateActivity.class);
167 addInviteUri(intent);
168 startActivity(intent);
169 } else {
170 binding.registerNewAccount.setText("Working...");
171 binding.registerNewAccount.setEnabled(false);
172 onboardingAccount = new Account(Jid.ofLocalAndDomain(UUID.randomUUID().toString(), Config.ONBOARDING_DOMAIN.toEscapedString()), CryptoHelper.createPassword(new SecureRandom()));
173 onboardingAccount.setOption(Account.OPTION_REGISTER, true);
174 onboardingAccount.setOption(Account.OPTION_FIXED_USERNAME, true);
175 xmppConnectionService.createAccount(onboardingAccount);
176 }
177 });
178 binding.useExisting.setOnClickListener(v -> {
179 final List<Account> accounts = xmppConnectionService.getAccounts();
180 Intent intent = new Intent(WelcomeActivity.this, EditAccountActivity.class);
181 intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, false);
182 if (accounts.size() == 1) {
183 intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString());
184 intent.putExtra("init", true);
185 } else if (accounts.size() >= 1) {
186 intent = new Intent(WelcomeActivity.this, ManageAccountActivity.class);
187 }
188 addInviteUri(intent);
189 startActivity(intent);
190 });
191 binding.useSnikket.setOnClickListener(v -> {
192 final List<Account> accounts = xmppConnectionService.getAccounts();
193 Intent intent = new Intent(WelcomeActivity.this, EditAccountActivity.class);
194 intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, false);
195 intent.putExtra("snikket", true);
196 if (accounts.size() == 1) {
197 intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString());
198 intent.putExtra("init", true);
199 } else if (accounts.size() >= 1) {
200 intent = new Intent(WelcomeActivity.this, ManageAccountActivity.class);
201 }
202 addInviteUri(intent);
203 startActivity(intent);
204 });
205
206 binding.useBackup.setOnClickListener(v -> {
207 if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
208 startActivity(new Intent(this, ImportBackupActivity.class));
209 }
210 });
211 }
212
213 @Override
214 public boolean onCreateOptionsMenu(Menu menu) {
215 getMenuInflater().inflate(R.menu.welcome_menu, menu);
216 final MenuItem scan = menu.findItem(R.id.action_scan_qr_code);
217 scan.setVisible(Compatibility.hasFeatureCamera(this));
218 return super.onCreateOptionsMenu(menu);
219 }
220
221
222
223 @Override
224 public boolean onOptionsItemSelected(MenuItem item) {
225 switch (item.getItemId()) {
226 case R.id.action_import_backup:
227 if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
228 startActivity(new Intent(this, ImportBackupActivity.class));
229 }
230 break;
231 case R.id.action_scan_qr_code:
232 UriHandlerActivity.scan(this, true);
233 break;
234 case R.id.action_add_account_with_cert:
235 addAccountFromKey();
236 break;
237 }
238 return super.onOptionsItemSelected(item);
239 }
240
241 private void addAccountFromKey() {
242 try {
243 KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
244 } catch (ActivityNotFoundException e) {
245 Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show();
246 }
247 }
248
249 @Override
250 public void alias(final String alias) {
251 if (alias != null) {
252 xmppConnectionService.createAccountFromKey(alias, this);
253 }
254 }
255
256 @Override
257 public void onAccountCreated(final Account account) {
258 final Intent intent = new Intent(this, EditAccountActivity.class);
259 intent.putExtra("jid", account.getJid().asBareJid().toEscapedString());
260 intent.putExtra("init", true);
261 addInviteUri(intent);
262 startActivity(intent);
263 }
264
265 @Override
266 public void informUser(final int r) {
267 runOnUiThread(() -> Toast.makeText(this, r, Toast.LENGTH_LONG).show());
268 }
269
270 @Override
271 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
272 UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
273 if (grantResults.length > 0) {
274 if (allGranted(grantResults)) {
275 switch (requestCode) {
276 case REQUEST_IMPORT_BACKUP:
277 startActivity(new Intent(this, ImportBackupActivity.class));
278 break;
279 }
280 } else if (Arrays.asList(permissions).contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
281 Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
282 }
283 }
284 if (writeGranted(grantResults, permissions)) {
285 if (xmppConnectionService != null) {
286 xmppConnectionService.restartFileObserver();
287 }
288 }
289 }
290
291 protected boolean hasInviteUri() {
292 final Intent from = getIntent();
293 if (from != null && from.hasExtra(StartConversationActivity.EXTRA_INVITE_URI)) return true;
294 return this.inviteUri != null;
295 }
296
297 public void addInviteUri(Intent to) {
298 final Intent from = getIntent();
299 if (from != null && from.hasExtra(StartConversationActivity.EXTRA_INVITE_URI)) {
300 final String invite = from.getStringExtra(StartConversationActivity.EXTRA_INVITE_URI);
301 to.putExtra(StartConversationActivity.EXTRA_INVITE_URI, invite);
302 } else if (this.inviteUri != null) {
303 Log.d(Config.LOGTAG, "injecting referrer uri into on-boarding flow");
304 to.putExtra(StartConversationActivity.EXTRA_INVITE_URI, this.inviteUri.toString());
305 }
306 }
307
308 class WelcomePagerAdapter extends PagerAdapter {
309 protected View[] pages;
310
311 public WelcomePagerAdapter(ViewPager p) {
312 super();
313 pages = new View[]{ p.getChildAt(0), p.getChildAt(1), p.getChildAt(2) };
314 for (View v : pages) {
315 p.removeView(v);
316 }
317 }
318
319 @Override
320 public int getCount() {
321 return 3;
322 }
323
324 @NonNull
325 @Override
326 public Object instantiateItem(@NonNull ViewGroup container, int position) {
327 container.addView(pages[position]);
328 return pages[position];
329 }
330
331 @Override
332 public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
333 return view == o;
334 }
335
336 @Override
337 public void destroyItem(@NonNull ViewGroup container, int position, Object o) {
338 container.removeView(pages[position]);
339 }
340 }
341}