Avoid potential negative array index access to cached text.
[LibreOffice.git] / android / source / src / java / org / libreoffice / ui / LibreOfficeUIActivity.java
blobbc5203d9c6eb1985d31459fa04ba907053dc6cb4
1 /* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 */
10 package org.libreoffice.ui;
12 import android.Manifest;
13 import android.content.ActivityNotFoundException;
14 import android.content.ComponentName;
15 import android.content.Context;
16 import android.content.Intent;
17 import android.content.SharedPreferences;
18 import android.content.pm.PackageManager;
19 import android.content.pm.ShortcutInfo;
20 import android.content.pm.ShortcutManager;
21 import android.graphics.drawable.Icon;
22 import android.net.Uri;
23 import android.os.Build;
24 import android.os.Bundle;
25 import android.preference.PreferenceManager;
26 import com.google.android.material.floatingactionbutton.FloatingActionButton;
27 import androidx.core.app.ActivityCompat;
28 import androidx.core.content.ContextCompat;
29 import androidx.core.view.ViewCompat;
30 import androidx.appcompat.app.ActionBar;
31 import androidx.appcompat.app.AppCompatActivity;
32 import androidx.recyclerview.widget.GridLayoutManager;
33 import androidx.recyclerview.widget.RecyclerView;
34 import androidx.appcompat.widget.Toolbar;
35 import android.text.TextUtils;
36 import android.util.Log;
37 import android.view.Menu;
38 import android.view.MenuInflater;
39 import android.view.MenuItem;
40 import android.view.View;
41 import android.view.animation.Animation;
42 import android.view.animation.AnimationUtils;
43 import android.view.animation.OvershootInterpolator;
44 import android.widget.LinearLayout;
45 import android.widget.TextView;
47 import org.libreoffice.AboutDialogFragment;
48 import org.libreoffice.BuildConfig;
49 import org.libreoffice.LibreOfficeMainActivity;
50 import org.libreoffice.LocaleHelper;
51 import org.libreoffice.R;
52 import org.libreoffice.SettingsActivity;
53 import org.libreoffice.SettingsListenerModel;
55 import java.util.ArrayList;
56 import java.util.Arrays;
57 import java.util.List;
59 public class LibreOfficeUIActivity extends AppCompatActivity implements SettingsListenerModel.OnSettingsPreferenceChangedListener, View.OnClickListener{
60 public enum DocumentType {
61 WRITER,
62 CALC,
63 IMPRESS,
64 DRAW,
65 INVALID
68 private static final String LOGTAG = LibreOfficeUIActivity.class.getSimpleName();
70 public static final String EXPLORER_PREFS_KEY = "EXPLORER_PREFS";
71 private static final String RECENT_DOCUMENTS_KEY = "RECENT_DOCUMENT_URIS";
72 // delimiter used for storing multiple URIs in a string
73 private static final String RECENT_DOCUMENTS_DELIMITER = " ";
74 private static final String DISPLAY_LANGUAGE = "DISPLAY_LANGUAGE";
76 public static final String NEW_DOC_TYPE_KEY = "NEW_DOC_TYPE_KEY";
77 public static final String NEW_WRITER_STRING_KEY = "private:factory/swriter";
78 public static final String NEW_IMPRESS_STRING_KEY = "private:factory/simpress";
79 public static final String NEW_CALC_STRING_KEY = "private:factory/scalc";
80 public static final String NEW_DRAW_STRING_KEY = "private:factory/sdraw";
82 // keep this in sync with 'AndroidManifext.xml'
83 private static final String[] SUPPORTED_MIME_TYPES = {
84 "application/vnd.oasis.opendocument.text",
85 "application/vnd.oasis.opendocument.graphics",
86 "application/vnd.oasis.opendocument.presentation",
87 "application/vnd.oasis.opendocument.spreadsheet",
88 "application/vnd.oasis.opendocument.text-flat-xml",
89 "application/vnd.oasis.opendocument.graphics-flat-xml",
90 "application/vnd.oasis.opendocument.presentation-flat-xml",
91 "application/vnd.oasis.opendocument.spreadsheet-flat-xml",
92 "application/vnd.oasis.opendocument.text-template",
93 "application/vnd.oasis.opendocument.spreadsheet-template",
94 "application/vnd.oasis.opendocument.graphics-template",
95 "application/vnd.oasis.opendocument.presentation-template",
96 "application/rtf",
97 "text/rtf",
98 "application/msword",
99 "application/vnd.ms-powerpoint",
100 "application/vnd.ms-excel",
101 "application/vnd.visio",
102 "application/vnd.visio.xml",
103 "application/x-mspublisher",
104 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
105 "application/vnd.openxmlformats-officedocument.presentationml.presentation",
106 "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
107 "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
108 "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
109 "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
110 "application/vnd.openxmlformats-officedocument.presentationml.template",
111 "text/csv",
112 "text/comma-separated-values",
113 "application/vnd.ms-works",
114 "application/vnd.apple.keynote",
115 "application/x-abiword",
116 "application/x-pagemaker",
117 "image/x-emf",
118 "image/x-svm",
119 "image/x-wmf",
120 "image/svg+xml",
123 private static final int REQUEST_CODE_OPEN_FILECHOOSER = 12345;
125 private static final int PERMISSION_WRITE_EXTERNAL_STORAGE = 0;
127 private Animation fabOpenAnimation;
128 private Animation fabCloseAnimation;
129 private boolean isFabMenuOpen = false;
130 private FloatingActionButton editFAB;
131 private FloatingActionButton writerFAB;
132 private FloatingActionButton drawFAB;
133 private FloatingActionButton impressFAB;
134 private FloatingActionButton calcFAB;
135 private LinearLayout drawLayout;
136 private LinearLayout writerLayout;
137 private LinearLayout impressLayout;
138 private LinearLayout calcLayout;
140 @Override
141 public void onCreate(Bundle savedInstanceState) {
142 super.onCreate(savedInstanceState);
144 readPreferences();
145 SettingsListenerModel.getInstance().setListener(this);
147 // init UI
148 createUI();
149 fabOpenAnimation = AnimationUtils.loadAnimation(this, R.anim.fab_open);
150 fabCloseAnimation = AnimationUtils.loadAnimation(this, R.anim.fab_close);
153 @Override
154 protected void onStart() {
155 super.onStart();
156 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
157 Log.i(LOGTAG, "no permission to read external storage - asking for permission");
158 ActivityCompat.requestPermissions(this,
159 new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
160 PERMISSION_WRITE_EXTERNAL_STORAGE);
164 @Override
165 protected void attachBaseContext(Context newBase) {
166 super.attachBaseContext(LocaleHelper.onAttach(newBase));
169 public void createUI() {
170 setContentView(R.layout.activity_document_browser);
172 Toolbar toolbar = findViewById(R.id.toolbar);
173 setSupportActionBar(toolbar);
174 ActionBar actionBar = getSupportActionBar();
176 if (actionBar != null) {
177 actionBar.setIcon(R.mipmap.ic_launcher);
180 editFAB = findViewById(R.id.editFAB);
181 editFAB.setOnClickListener(this);
182 // allow creating new docs only when experimental editing is enabled
183 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
184 final boolean bEditingEnabled = BuildConfig.ALLOW_EDITING && preferences.getBoolean(LibreOfficeMainActivity.ENABLE_EXPERIMENTAL_PREFS_KEY, false);
185 editFAB.setVisibility(bEditingEnabled ? View.VISIBLE : View.INVISIBLE);
187 impressFAB = findViewById(R.id.newImpressFAB);
188 impressFAB.setOnClickListener(this);
189 writerFAB = findViewById(R.id.newWriterFAB);
190 writerFAB.setOnClickListener(this);
191 calcFAB = findViewById(R.id.newCalcFAB);
192 calcFAB.setOnClickListener(this);
193 drawFAB = findViewById(R.id.newDrawFAB);
194 drawFAB.setOnClickListener(this);
195 writerLayout = findViewById(R.id.writerLayout);
196 impressLayout = findViewById(R.id.impressLayout);
197 calcLayout = findViewById(R.id.calcLayout);
198 drawLayout = findViewById(R.id.drawLayout);
199 TextView openFileView = findViewById(R.id.open_file_button);
200 openFileView.setOnClickListener(this);
203 RecyclerView recentRecyclerView = findViewById(R.id.list_recent);
205 SharedPreferences prefs = getSharedPreferences(EXPLORER_PREFS_KEY, MODE_PRIVATE);
206 String recentPref = prefs.getString(RECENT_DOCUMENTS_KEY, "");
207 String[] recentFileStrings = recentPref.split(RECENT_DOCUMENTS_DELIMITER);
209 final List<RecentFile> recentFiles = new ArrayList<>();
210 for (String recentFileString : recentFileStrings) {
211 Uri uri = Uri.parse(recentFileString);
212 String filename = FileUtilities.retrieveDisplayNameForDocumentUri(getContentResolver(), uri);
213 if (!filename.isEmpty()) {
214 recentFiles.add(new RecentFile(uri, filename));
218 recentRecyclerView.setLayoutManager(new GridLayoutManager(this, 2));
219 recentRecyclerView.setAdapter(new RecentFilesAdapter(this, recentFiles));
222 private void expandFabMenu() {
223 ViewCompat.animate(editFAB).rotation(45.0F).withLayer().setDuration(300).setInterpolator(new OvershootInterpolator(10.0F)).start();
224 drawLayout.startAnimation(fabOpenAnimation);
225 impressLayout.startAnimation(fabOpenAnimation);
226 writerLayout.startAnimation(fabOpenAnimation);
227 calcLayout.startAnimation(fabOpenAnimation);
228 writerFAB.setClickable(true);
229 impressFAB.setClickable(true);
230 drawFAB.setClickable(true);
231 calcFAB.setClickable(true);
232 isFabMenuOpen = true;
235 private void collapseFabMenu() {
236 ViewCompat.animate(editFAB).rotation(0.0F).withLayer().setDuration(300).setInterpolator(new OvershootInterpolator(10.0F)).start();
237 writerLayout.startAnimation(fabCloseAnimation);
238 impressLayout.startAnimation(fabCloseAnimation);
239 drawLayout.startAnimation(fabCloseAnimation);
240 calcLayout.startAnimation(fabCloseAnimation);
241 writerFAB.setClickable(false);
242 impressFAB.setClickable(false);
243 drawFAB.setClickable(false);
244 calcFAB.setClickable(false);
245 isFabMenuOpen = false;
248 @Override
249 public void onBackPressed() {
250 if (isFabMenuOpen) {
251 collapseFabMenu();
252 } else {
253 super.onBackPressed();
257 @Override
258 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
259 super.onActivityResult(requestCode, resultCode, data);
260 if (requestCode == REQUEST_CODE_OPEN_FILECHOOSER && resultCode == RESULT_OK) {
261 final Uri fileUri = data.getData();
262 openDocument(fileUri);
266 private void showSystemFilePickerAndOpenFile() {
267 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
268 intent.setType("*/*");
269 intent.putExtra(Intent.EXTRA_MIME_TYPES, SUPPORTED_MIME_TYPES);
271 try {
272 startActivityForResult(intent, REQUEST_CODE_OPEN_FILECHOOSER);
273 } catch (ActivityNotFoundException e) {
274 Log.w(LOGTAG, "No activity available that can handle the intent to open a document.");
278 public void openDocument(final Uri documentUri) {
279 // "forward" to LibreOfficeMainActivity to open the file
280 Intent intent = new Intent(Intent.ACTION_VIEW, documentUri);
281 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
283 addDocumentToRecents(documentUri);
285 String packageName = getApplicationContext().getPackageName();
286 ComponentName componentName = new ComponentName(packageName,
287 LibreOfficeMainActivity.class.getName());
288 intent.setComponent(componentName);
289 startActivity(intent);
292 private void loadNewDocument(DocumentType docType) {
293 final String newDocumentType;
294 if (docType == DocumentType.WRITER) {
295 newDocumentType = NEW_WRITER_STRING_KEY;
296 } else if (docType == DocumentType.CALC) {
297 newDocumentType = NEW_CALC_STRING_KEY;
298 } else if (docType == DocumentType.IMPRESS) {
299 newDocumentType = NEW_IMPRESS_STRING_KEY;
300 } else if (docType == DocumentType.DRAW) {
301 newDocumentType = NEW_DRAW_STRING_KEY;
302 } else {
303 Log.w(LOGTAG, "invalid document type passed to loadNewDocument method. Ignoring request");
304 return;
307 Intent intent = new Intent(LibreOfficeUIActivity.this, LibreOfficeMainActivity.class);
308 intent.putExtra(NEW_DOC_TYPE_KEY, newDocumentType);
309 startActivity(intent);
312 @Override
313 public boolean onCreateOptionsMenu(Menu menu) {
314 MenuInflater inflater = getMenuInflater();
315 inflater.inflate(R.menu.view_menu, menu);
317 return true;
320 @Override
321 public boolean onOptionsItemSelected(MenuItem item) {
322 final int itemId = item.getItemId();
323 if (itemId == R.id.action_about) {
324 AboutDialogFragment aboutDialogFragment = new AboutDialogFragment();
325 aboutDialogFragment.show(getSupportFragmentManager(), "AboutDialogFragment");
326 return true;
328 if (itemId == R.id.action_settings) {
329 startActivity(new Intent(getApplicationContext(), SettingsActivity.class));
330 return true;
333 return super.onOptionsItemSelected(item);
336 public void readPreferences(){
337 SharedPreferences defaultPrefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
338 final String displayLanguage = defaultPrefs.getString(DISPLAY_LANGUAGE, LocaleHelper.SYSTEM_DEFAULT_LANGUAGE);
339 LocaleHelper.setLocale(this, displayLanguage);
342 @Override
343 public void settingsPreferenceChanged(SharedPreferences sharedPreferences, String key) {
344 readPreferences();
347 @Override
348 protected void onResume() {
349 super.onResume();
350 Log.d(LOGTAG, "onResume");
351 createUI();
354 private void addDocumentToRecents(Uri fileUri) {
355 SharedPreferences prefs = getSharedPreferences(EXPLORER_PREFS_KEY, MODE_PRIVATE);
357 // preserve permissions across device reboots,
358 // s. https://developer.android.com/training/data-storage/shared/documents-files#persist-permissions
359 getContentResolver().takePersistableUriPermission(fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
361 String newRecent = fileUri.toString();
362 List<String> recentsList = new ArrayList<>(Arrays.asList(prefs.getString(RECENT_DOCUMENTS_KEY, "").split(RECENT_DOCUMENTS_DELIMITER)));
364 // remove string if present, so that it doesn't appear multiple times
365 recentsList.remove(newRecent);
367 // put the new value in the first place
368 recentsList.add(0, newRecent);
371 * 4 because the number of recommended items in App Shortcuts is 4, and also
372 * because it's a good number of recent items in general
374 final int RECENTS_SIZE = 4;
376 while (recentsList.size() > RECENTS_SIZE) {
377 recentsList.remove(RECENTS_SIZE);
380 // serialize to String that can be set for pref
381 String value = TextUtils.join(RECENT_DOCUMENTS_DELIMITER, recentsList);
382 prefs.edit().putString(RECENT_DOCUMENTS_KEY, value).apply();
384 //update app shortcuts (7.0 and above)
385 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
386 ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
388 //Remove all shortcuts, and apply new ones.
389 shortcutManager.removeAllDynamicShortcuts();
391 ArrayList<ShortcutInfo> shortcuts = new ArrayList<>();
392 for (String recentDoc : recentsList) {
393 Uri docUri = Uri.parse(recentDoc);
394 String filename = FileUtilities.retrieveDisplayNameForDocumentUri(getContentResolver(), docUri);
395 if (filename.isEmpty()) {
396 continue;
399 //find the appropriate drawable
400 int drawable = 0;
401 switch (FileUtilities.getType(filename)) {
402 case FileUtilities.DOC:
403 drawable = R.drawable.writer;
404 break;
405 case FileUtilities.CALC:
406 drawable = R.drawable.calc;
407 break;
408 case FileUtilities.DRAWING:
409 drawable = R.drawable.draw;
410 break;
411 case FileUtilities.IMPRESS:
412 drawable = R.drawable.impress;
413 break;
416 Intent intent = new Intent(Intent.ACTION_VIEW, docUri);
417 String packageName = this.getApplicationContext().getPackageName();
418 ComponentName componentName = new ComponentName(packageName, LibreOfficeMainActivity.class.getName());
419 intent.setComponent(componentName);
421 ShortcutInfo shortcut = new ShortcutInfo.Builder(this, filename)
422 .setShortLabel(filename)
423 .setLongLabel(filename)
424 .setIcon(Icon.createWithResource(this, drawable))
425 .setIntent(intent)
426 .build();
428 shortcuts.add(shortcut);
430 shortcutManager.setDynamicShortcuts(shortcuts);
434 @Override
435 public void onClick(View v) {
436 int id = v.getId();
437 if (id == R.id.editFAB) {
438 if (isFabMenuOpen) {
439 collapseFabMenu();
440 } else {
441 expandFabMenu();
443 } else if (id == R.id.open_file_button) {
444 showSystemFilePickerAndOpenFile();
445 } else if (id == R.id.newWriterFAB) {
446 loadNewDocument(DocumentType.WRITER);
447 } else if (id == R.id.newImpressFAB) {
448 loadNewDocument(DocumentType.IMPRESS);
449 } else if (id == R.id.newCalcFAB) {
450 loadNewDocument(DocumentType.CALC);
451 } else if (id == R.id.newDrawFAB) {
452 loadNewDocument(DocumentType.DRAW);
457 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */