SyncedFoldersActivity.java 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871
  1. /*
  2. * Nextcloud Android client application
  3. *
  4. * @author Andy Scherzinger
  5. * Copyright (C) 2016 Andy Scherzinger
  6. * Copyright (C) 2016 Nextcloud
  7. *
  8. * This program is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  10. * License as published by the Free Software Foundation; either
  11. * version 3 of the License, or any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public
  19. * License along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. */
  21. package com.owncloud.android.ui.activity;
  22. import android.accounts.Account;
  23. import android.annotation.SuppressLint;
  24. import android.app.Activity;
  25. import android.app.NotificationManager;
  26. import android.content.Context;
  27. import android.content.Intent;
  28. import android.content.pm.PackageManager;
  29. import android.net.Uri;
  30. import android.os.Build;
  31. import android.os.Bundle;
  32. import android.os.PowerManager;
  33. import android.text.TextUtils;
  34. import android.util.Log;
  35. import android.view.Menu;
  36. import android.view.MenuInflater;
  37. import android.view.MenuItem;
  38. import android.view.View;
  39. import android.widget.ImageView;
  40. import android.widget.LinearLayout;
  41. import android.widget.ProgressBar;
  42. import android.widget.TextView;
  43. import com.nextcloud.client.account.User;
  44. import com.nextcloud.client.core.Clock;
  45. import com.nextcloud.client.device.PowerManagementService;
  46. import com.nextcloud.client.di.Injectable;
  47. import com.nextcloud.client.preferences.AppPreferences;
  48. import com.nextcloud.java.util.Optional;
  49. import com.owncloud.android.BuildConfig;
  50. import com.owncloud.android.MainApp;
  51. import com.owncloud.android.R;
  52. import com.owncloud.android.datamodel.ArbitraryDataProvider;
  53. import com.owncloud.android.datamodel.MediaFolder;
  54. import com.owncloud.android.datamodel.MediaFolderType;
  55. import com.owncloud.android.datamodel.MediaProvider;
  56. import com.owncloud.android.datamodel.OCFile;
  57. import com.owncloud.android.datamodel.SyncedFolder;
  58. import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
  59. import com.owncloud.android.datamodel.SyncedFolderProvider;
  60. import com.owncloud.android.files.services.FileUploader;
  61. import com.owncloud.android.jobs.MediaFoldersDetectionJob;
  62. import com.owncloud.android.jobs.NotificationJob;
  63. import com.owncloud.android.ui.adapter.SyncedFolderAdapter;
  64. import com.owncloud.android.ui.decoration.MediaGridItemDecoration;
  65. import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment;
  66. import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable;
  67. import com.owncloud.android.utils.DisplayUtils;
  68. import com.owncloud.android.utils.FilesSyncHelper;
  69. import com.owncloud.android.utils.PermissionUtil;
  70. import com.owncloud.android.utils.ThemeUtils;
  71. import java.io.File;
  72. import java.util.ArrayList;
  73. import java.util.Arrays;
  74. import java.util.Collections;
  75. import java.util.HashMap;
  76. import java.util.List;
  77. import java.util.Locale;
  78. import java.util.Map;
  79. import javax.inject.Inject;
  80. import androidx.annotation.NonNull;
  81. import androidx.appcompat.app.ActionBar;
  82. import androidx.appcompat.app.AlertDialog;
  83. import androidx.drawerlayout.widget.DrawerLayout;
  84. import androidx.fragment.app.FragmentManager;
  85. import androidx.fragment.app.FragmentTransaction;
  86. import androidx.recyclerview.widget.GridLayoutManager;
  87. import androidx.recyclerview.widget.RecyclerView;
  88. import butterknife.BindView;
  89. import butterknife.ButterKnife;
  90. import butterknife.OnClick;
  91. import static android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS;
  92. import static com.owncloud.android.datamodel.SyncedFolderDisplayItem.UNPERSISTED_ID;
  93. /**
  94. * Activity displaying all auto-synced folders and/or instant upload media folders.
  95. */
  96. public class SyncedFoldersActivity extends FileActivity implements SyncedFolderAdapter.ClickListener,
  97. SyncedFolderPreferencesDialogFragment.OnSyncedFolderPreferenceListener, Injectable {
  98. private static final String[] PRIORITIZED_FOLDERS = new String[]{"Camera", "Screenshots"};
  99. public static final String EXTRA_SHOW_SIDEBAR = "SHOW_SIDEBAR";
  100. private static final String SYNCED_FOLDER_PREFERENCES_DIALOG_TAG = "SYNCED_FOLDER_PREFERENCES_DIALOG";
  101. private static final String TAG = SyncedFoldersActivity.class.getSimpleName();
  102. @BindView(R.id.empty_list_view)
  103. public LinearLayout emptyContentContainer;
  104. @BindView(R.id.empty_list_icon)
  105. public ImageView emptyContentIcon;
  106. @BindView(R.id.empty_list_progress)
  107. public ProgressBar emptyContentProgressBar;
  108. @BindView(R.id.empty_list_view_headline)
  109. public TextView emptyContentHeadline;
  110. @BindView(R.id.empty_list_view_text)
  111. public TextView emptyContentMessage;
  112. @BindView(android.R.id.list)
  113. public RecyclerView mRecyclerView;
  114. private SyncedFolderAdapter mAdapter;
  115. private SyncedFolderProvider mSyncedFolderProvider;
  116. private SyncedFolderPreferencesDialogFragment mSyncedFolderPreferencesDialogFragment;
  117. private boolean showSidebar = true;
  118. private String path;
  119. private int type;
  120. @Inject AppPreferences preferences;
  121. @Inject PowerManagementService powerManagementService;
  122. @Inject Clock clock;
  123. @Override
  124. protected void onCreate(Bundle savedInstanceState) {
  125. super.onCreate(savedInstanceState);
  126. if (getIntent().getExtras() != null) {
  127. showSidebar = getIntent().getExtras().getBoolean(EXTRA_SHOW_SIDEBAR);
  128. }
  129. setContentView(R.layout.synced_folders_layout);
  130. ButterKnife.bind(this);
  131. if (getIntent() != null && getIntent().getExtras() != null) {
  132. final String accountName = getIntent().getExtras().getString(NotificationJob.KEY_NOTIFICATION_ACCOUNT);
  133. Optional<User> optionalUser = getUser();
  134. if (optionalUser.isPresent() && accountName != null) {
  135. User user = optionalUser.get();
  136. if (!accountName.equalsIgnoreCase(user.getAccountName())) {
  137. accountManager.setCurrentOwnCloudAccount(accountName);
  138. setUser(getUserAccountManager().getUser());
  139. }
  140. }
  141. path = getIntent().getStringExtra(MediaFoldersDetectionJob.KEY_MEDIA_FOLDER_PATH);
  142. type = getIntent().getIntExtra(MediaFoldersDetectionJob.KEY_MEDIA_FOLDER_TYPE, -1);
  143. // Cancel notification
  144. int notificationId = getIntent().getIntExtra(MediaFoldersDetectionJob.NOTIFICATION_ID, 0);
  145. NotificationManager notificationManager =
  146. (NotificationManager) getSystemService(Activity.NOTIFICATION_SERVICE);
  147. notificationManager.cancel(notificationId);
  148. }
  149. // setup toolbar
  150. setupToolbar();
  151. if (getSupportActionBar() != null){
  152. getSupportActionBar().setTitle(R.string.drawer_synced_folders);
  153. }
  154. // setup drawer
  155. setupDrawer(R.id.nav_synced_folders);
  156. if (!showSidebar) {
  157. setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
  158. mDrawerToggle.setDrawerIndicatorEnabled(false);
  159. }
  160. setupContent();
  161. ActionBar actionBar = getSupportActionBar();
  162. if (actionBar != null) {
  163. ThemeUtils.setColoredTitle(getSupportActionBar(), getString(R.string.drawer_synced_folders), this);
  164. actionBar.setDisplayHomeAsUpEnabled(true);
  165. }
  166. if (ThemeUtils.themingEnabled(this)) {
  167. setTheme(R.style.FallbackThemingTheme);
  168. }
  169. }
  170. @Override
  171. public boolean onCreateOptionsMenu(Menu menu) {
  172. MenuInflater inflater = getMenuInflater();
  173. inflater.inflate(R.menu.activity_synced_folders, menu);
  174. if (powerManagementService.isPowerSavingExclusionAvailable()) {
  175. MenuItem item = menu.findItem(R.id.action_disable_power_save_check);
  176. item.setVisible(true);
  177. item.setChecked(preferences.isPowerCheckDisabled());
  178. item.setOnMenuItemClickListener(this::onDisablePowerSaveCheckClicked);
  179. }
  180. return true;
  181. }
  182. private boolean onDisablePowerSaveCheckClicked(MenuItem powerCheck) {
  183. if (!powerCheck.isChecked()) {
  184. showPowerCheckDialog();
  185. }
  186. preferences.setPowerCheckDisabled(!powerCheck.isChecked());
  187. powerCheck.setChecked(!powerCheck.isChecked());
  188. return true;
  189. }
  190. private void showPowerCheckDialog() {
  191. AlertDialog alertDialog = new AlertDialog.Builder(this)
  192. .setView(findViewById(R.id.root_layout))
  193. .setPositiveButton(R.string.common_ok, (dialog, which) -> dialog.dismiss())
  194. .setTitle(ThemeUtils.getColoredTitle(getResources().getString(R.string.autoupload_disable_power_save_check),
  195. ThemeUtils.primaryAccentColor(this)))
  196. .setMessage(getString(R.string.power_save_check_dialog_message))
  197. .show();
  198. alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ThemeUtils.primaryAccentColor(this));
  199. }
  200. /**
  201. * sets up the UI elements and loads all media/synced folders.
  202. */
  203. private void setupContent() {
  204. final int gridWidth = getResources().getInteger(R.integer.media_grid_width);
  205. boolean lightVersion = getResources().getBoolean(R.bool.syncedFolder_light);
  206. mAdapter = new SyncedFolderAdapter(this, clock, gridWidth, this, lightVersion);
  207. mSyncedFolderProvider = new SyncedFolderProvider(getContentResolver(), preferences, clock);
  208. emptyContentIcon.setImageResource(R.drawable.nav_synced_folders);
  209. final GridLayoutManager lm = new GridLayoutManager(this, gridWidth);
  210. mAdapter.setLayoutManager(lm);
  211. int spacing = getResources().getDimensionPixelSize(R.dimen.media_grid_spacing);
  212. mRecyclerView.addItemDecoration(new MediaGridItemDecoration(spacing));
  213. mRecyclerView.setLayoutManager(lm);
  214. mRecyclerView.setAdapter(mAdapter);
  215. load(gridWidth * 2, false);
  216. }
  217. @OnClick(R.id.empty_list_view_text)
  218. public void showHiddenItems() {
  219. if (mAdapter.getSectionCount() == 0 && mAdapter.getUnfilteredSectionCount() > mAdapter.getSectionCount()) {
  220. mAdapter.toggleHiddenItemsVisibility();
  221. emptyContentContainer.setVisibility(View.GONE);
  222. mRecyclerView.setVisibility(View.VISIBLE);
  223. }
  224. }
  225. /**
  226. * loads all media/synced folders, adds them to the recycler view adapter and shows the list.
  227. *
  228. * @param perFolderMediaItemLimit the amount of media items to be loaded/shown per media folder
  229. */
  230. private void load(final int perFolderMediaItemLimit, boolean force) {
  231. if (mAdapter.getItemCount() > 0 && !force) {
  232. return;
  233. }
  234. showLoadingContent();
  235. final List<MediaFolder> mediaFolders = MediaProvider.getImageFolders(getContentResolver(),
  236. perFolderMediaItemLimit, this, false);
  237. mediaFolders.addAll(MediaProvider.getVideoFolders(getContentResolver(), perFolderMediaItemLimit,
  238. this, false));
  239. List<SyncedFolder> syncedFolderArrayList = mSyncedFolderProvider.getSyncedFolders();
  240. List<SyncedFolder> currentAccountSyncedFoldersList = new ArrayList<>();
  241. User user = getUserAccountManager().getUser();
  242. for (SyncedFolder syncedFolder : syncedFolderArrayList) {
  243. if (syncedFolder.getAccount().equals(user.getAccountName())) {
  244. // delete non-existing & disabled synced folders
  245. if (!new File(syncedFolder.getLocalPath()).exists() && !syncedFolder.isEnabled()) {
  246. mSyncedFolderProvider.deleteSyncedFolder(syncedFolder.getId());
  247. } else {
  248. currentAccountSyncedFoldersList.add(syncedFolder);
  249. }
  250. }
  251. }
  252. List<SyncedFolderDisplayItem> syncFolderItems = sortSyncedFolderItems(
  253. mergeFolderData(currentAccountSyncedFoldersList, mediaFolders));
  254. mAdapter.setSyncFolderItems(syncFolderItems);
  255. mAdapter.notifyDataSetChanged();
  256. showList();
  257. if (!TextUtils.isEmpty(path)) {
  258. int section = mAdapter.getSectionByLocalPathAndType(path, type);
  259. if (section >= 0) {
  260. onSyncFolderSettingsClick(section, mAdapter.get(section));
  261. }
  262. }
  263. }
  264. /**
  265. * Sorts list of {@link SyncedFolderDisplayItem}s.
  266. *
  267. * @param syncFolderItemList list of items to be sorted
  268. * @return sorted list of items
  269. */
  270. public static List<SyncedFolderDisplayItem> sortSyncedFolderItems(List<SyncedFolderDisplayItem>
  271. syncFolderItemList) {
  272. Collections.sort(syncFolderItemList, (f1, f2) -> {
  273. if (f1 == null && f2 == null) {
  274. return 0;
  275. } else if (f1 == null) {
  276. return -1;
  277. } else if (f2 == null) {
  278. return 1;
  279. } else if (f1.isEnabled() && f2.isEnabled()) {
  280. if (f1.getFolderName() == null) {
  281. return -1;
  282. }
  283. if (f2.getFolderName() == null) {
  284. return 1;
  285. }
  286. return f1.getFolderName().toLowerCase(Locale.getDefault()).compareTo(
  287. f2.getFolderName().toLowerCase(Locale.getDefault()));
  288. } else if (f1.getFolderName() == null && f2.getFolderName() == null) {
  289. return 0;
  290. } else if (f1.isEnabled()) {
  291. return -1;
  292. } else if (f2.isEnabled()) {
  293. return 1;
  294. } else if (f1.getFolderName() == null) {
  295. return -1;
  296. } else if (f2.getFolderName() == null) {
  297. return 1;
  298. }
  299. for (String folder : PRIORITIZED_FOLDERS) {
  300. if (folder.equals(f1.getFolderName()) && folder.equals(f2.getFolderName())) {
  301. return 0;
  302. } else if (folder.equals(f1.getFolderName())) {
  303. return -1;
  304. } else if (folder.equals(f2.getFolderName())) {
  305. return 1;
  306. }
  307. }
  308. return f1.getFolderName().toLowerCase(Locale.getDefault()).compareTo(
  309. f2.getFolderName().toLowerCase(Locale.getDefault()));
  310. });
  311. return syncFolderItemList;
  312. }
  313. /**
  314. * merges two lists of {@link SyncedFolder} and {@link MediaFolder} items into one of SyncedFolderItems.
  315. *
  316. * @param syncedFolders the synced folders
  317. * @param mediaFolders the media folders
  318. * @return the merged list of SyncedFolderItems
  319. */
  320. @NonNull
  321. private List<SyncedFolderDisplayItem> mergeFolderData(List<SyncedFolder> syncedFolders,
  322. @NonNull List<MediaFolder> mediaFolders) {
  323. Map<String, SyncedFolder> syncedFoldersMap = createSyncedFoldersMap(syncedFolders);
  324. List<SyncedFolderDisplayItem> result = new ArrayList<>();
  325. for (MediaFolder mediaFolder : mediaFolders) {
  326. if (syncedFoldersMap.containsKey(mediaFolder.absolutePath + "-" + mediaFolder.type)) {
  327. SyncedFolder syncedFolder = syncedFoldersMap.get(mediaFolder.absolutePath + "-" + mediaFolder.type);
  328. syncedFoldersMap.remove(mediaFolder.absolutePath + "-" + mediaFolder.type);
  329. if (MediaFolderType.CUSTOM == syncedFolder.getType()) {
  330. result.add(createSyncedFolderWithoutMediaFolder(syncedFolder));
  331. } else {
  332. result.add(createSyncedFolder(syncedFolder, mediaFolder));
  333. }
  334. } else {
  335. result.add(createSyncedFolderFromMediaFolder(mediaFolder));
  336. }
  337. }
  338. for (SyncedFolder syncedFolder : syncedFoldersMap.values()) {
  339. result.add(createSyncedFolderWithoutMediaFolder(syncedFolder));
  340. }
  341. return result;
  342. }
  343. @NonNull
  344. private SyncedFolderDisplayItem createSyncedFolderWithoutMediaFolder(@NonNull SyncedFolder syncedFolder) {
  345. File localFolder = new File(syncedFolder.getLocalPath());
  346. File[] files = getFileList(localFolder);
  347. List<String> filePaths = getDisplayFilePathList(files);
  348. return new SyncedFolderDisplayItem(
  349. syncedFolder.getId(),
  350. syncedFolder.getLocalPath(),
  351. syncedFolder.getRemotePath(),
  352. syncedFolder.isWifiOnly(),
  353. syncedFolder.isChargingOnly(),
  354. syncedFolder.isSubfolderByDate(),
  355. syncedFolder.getAccount(),
  356. syncedFolder.getUploadAction(),
  357. syncedFolder.isEnabled(),
  358. clock.getCurrentTime(),
  359. filePaths,
  360. localFolder.getName(),
  361. files.length,
  362. syncedFolder.getType(),
  363. syncedFolder.isHidden());
  364. }
  365. /**
  366. * creates a SyncedFolderDisplayItem merging a {@link SyncedFolder} and a {@link MediaFolder} object instance.
  367. *
  368. * @param syncedFolder the synced folder object
  369. * @param mediaFolder the media folder object
  370. * @return the created SyncedFolderDisplayItem
  371. */
  372. @NonNull
  373. private SyncedFolderDisplayItem createSyncedFolder(@NonNull SyncedFolder syncedFolder, @NonNull MediaFolder mediaFolder) {
  374. return new SyncedFolderDisplayItem(
  375. syncedFolder.getId(),
  376. syncedFolder.getLocalPath(),
  377. syncedFolder.getRemotePath(),
  378. syncedFolder.isWifiOnly(),
  379. syncedFolder.isChargingOnly(),
  380. syncedFolder.isSubfolderByDate(),
  381. syncedFolder.getAccount(),
  382. syncedFolder.getUploadAction(),
  383. syncedFolder.isEnabled(),
  384. clock.getCurrentTime(),
  385. mediaFolder.filePaths,
  386. mediaFolder.folderName,
  387. mediaFolder.numberOfFiles,
  388. mediaFolder.type,
  389. syncedFolder.isHidden());
  390. }
  391. /**
  392. * creates a {@link SyncedFolderDisplayItem} based on a {@link MediaFolder} object instance.
  393. *
  394. * @param mediaFolder the media folder object
  395. * @return the created SyncedFolderDisplayItem
  396. */
  397. @NonNull
  398. private SyncedFolderDisplayItem createSyncedFolderFromMediaFolder(@NonNull MediaFolder mediaFolder) {
  399. return new SyncedFolderDisplayItem(
  400. UNPERSISTED_ID,
  401. mediaFolder.absolutePath,
  402. getString(R.string.instant_upload_path) + "/" + mediaFolder.folderName,
  403. true,
  404. false,
  405. false,
  406. getAccount().name,
  407. FileUploader.LOCAL_BEHAVIOUR_FORGET,
  408. false,
  409. clock.getCurrentTime(),
  410. mediaFolder.filePaths,
  411. mediaFolder.folderName,
  412. mediaFolder.numberOfFiles,
  413. mediaFolder.type,
  414. false);
  415. }
  416. private File[] getFileList(File localFolder) {
  417. File[] files = localFolder.listFiles(pathname -> !pathname.isDirectory());
  418. if (files != null) {
  419. Arrays.sort(files, (f1, f2) -> Long.compare(f1.lastModified(), f2.lastModified()));
  420. } else {
  421. files = new File[]{};
  422. }
  423. return files;
  424. }
  425. private List<String> getDisplayFilePathList(File... files) {
  426. List<String> filePaths = null;
  427. if (files != null && files.length > 0) {
  428. filePaths = new ArrayList<>();
  429. for (int i = 0; i < 7 && i < files.length; i++) {
  430. filePaths.add(files[i].getAbsolutePath());
  431. }
  432. }
  433. return filePaths;
  434. }
  435. /**
  436. * creates a lookup map for a list of given {@link SyncedFolder}s with their local path as the key.
  437. *
  438. * @param syncFolders list of {@link SyncedFolder}s
  439. * @return the lookup map for {@link SyncedFolder}s
  440. */
  441. @NonNull
  442. private Map<String, SyncedFolder> createSyncedFoldersMap(List<SyncedFolder> syncFolders) {
  443. Map<String, SyncedFolder> result = new HashMap<>();
  444. if (syncFolders != null) {
  445. for (SyncedFolder syncFolder : syncFolders) {
  446. result.put(syncFolder.getLocalPath() + "-" + syncFolder.getType(), syncFolder);
  447. }
  448. }
  449. return result;
  450. }
  451. /**
  452. * show recycler view list or the empty message info (in case list is empty).
  453. */
  454. private void showList() {
  455. if (mRecyclerView != null) {
  456. mRecyclerView.setVisibility(View.VISIBLE );
  457. emptyContentProgressBar.setVisibility(View.GONE);
  458. if (mAdapter.getSectionCount() == 0 && mAdapter.getUnfilteredSectionCount() > mAdapter.getSectionCount()) {
  459. emptyContentContainer.setVisibility(View.VISIBLE);
  460. int hiddenFoldersCount = mAdapter.getHiddenFolderCount();
  461. showEmptyContent(getString(R.string.drawer_synced_folders),
  462. getResources().getQuantityString(R.plurals.synced_folders_show_hidden_folders,
  463. hiddenFoldersCount,
  464. hiddenFoldersCount));
  465. } else if (mAdapter.getSectionCount() == 0 && mAdapter.getUnfilteredSectionCount() == 0) {
  466. emptyContentContainer.setVisibility(View.VISIBLE);
  467. showEmptyContent(getString(R.string.drawer_synced_folders),
  468. getString(R.string.synced_folders_no_results));
  469. } else {
  470. emptyContentContainer.setVisibility(View.GONE);
  471. }
  472. }
  473. }
  474. @Override
  475. public boolean onOptionsItemSelected(MenuItem item) {
  476. boolean result = true;
  477. switch (item.getItemId()) {
  478. case android.R.id.home: {
  479. if (showSidebar) {
  480. if (isDrawerOpen()) {
  481. closeDrawer();
  482. } else {
  483. openDrawer();
  484. }
  485. } else {
  486. Intent settingsIntent = new Intent(getApplicationContext(), SettingsActivity.class);
  487. startActivity(settingsIntent);
  488. }
  489. break;
  490. }
  491. case R.id.action_create_custom_folder: {
  492. Log.d(TAG, "Show custom folder dialog");
  493. SyncedFolderDisplayItem emptyCustomFolder = new SyncedFolderDisplayItem(
  494. SyncedFolder.UNPERSISTED_ID, null, null, true, false,
  495. false, getAccount().name, FileUploader.LOCAL_BEHAVIOUR_FORGET, false,
  496. clock.getCurrentTime(), null, MediaFolderType.CUSTOM, false);
  497. onSyncFolderSettingsClick(0, emptyCustomFolder);
  498. }
  499. default:
  500. result = super.onOptionsItemSelected(item);
  501. break;
  502. }
  503. return result;
  504. }
  505. @Override
  506. public void restart() {
  507. Intent i = new Intent(this, FileDisplayActivity.class);
  508. i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
  509. startActivity(i);
  510. }
  511. @Override
  512. public void showFiles(boolean onDeviceOnly) {
  513. MainApp.showOnlyFilesOnDevice(onDeviceOnly);
  514. Intent fileDisplayActivity = new Intent(getApplicationContext(), FileDisplayActivity.class);
  515. fileDisplayActivity.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
  516. startActivity(fileDisplayActivity);
  517. }
  518. @Override
  519. public void onSyncStatusToggleClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem) {
  520. if (syncedFolderDisplayItem.getId() > UNPERSISTED_ID) {
  521. mSyncedFolderProvider.updateSyncedFolderEnabled(syncedFolderDisplayItem.getId(),
  522. syncedFolderDisplayItem.isEnabled());
  523. } else {
  524. long storedId = mSyncedFolderProvider.storeSyncedFolder(syncedFolderDisplayItem);
  525. if (storedId != -1) {
  526. syncedFolderDisplayItem.setId(storedId);
  527. }
  528. }
  529. if (syncedFolderDisplayItem.isEnabled()) {
  530. FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolderDisplayItem);
  531. showBatteryOptimizationInfo();
  532. }
  533. }
  534. @Override
  535. public void onSyncFolderSettingsClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem) {
  536. FragmentManager fm = getSupportFragmentManager();
  537. FragmentTransaction ft = fm.beginTransaction();
  538. ft.addToBackStack(null);
  539. mSyncedFolderPreferencesDialogFragment = SyncedFolderPreferencesDialogFragment.newInstance(
  540. syncedFolderDisplayItem, section);
  541. mSyncedFolderPreferencesDialogFragment.show(ft, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG);
  542. }
  543. @Override
  544. public void onVisibilityToggleClick(int section, SyncedFolderDisplayItem syncedFolder) {
  545. syncedFolder.setHidden(!syncedFolder.isHidden());
  546. saveOrUpdateSyncedFolder(syncedFolder);
  547. mAdapter.setSyncFolderItem(section, syncedFolder);
  548. }
  549. private void showEmptyContent(String headline, String message) {
  550. showEmptyContent(headline, message, false);
  551. }
  552. private void showLoadingContent() {
  553. showEmptyContent(
  554. getString(R.string.drawer_synced_folders),
  555. getString(R.string.synced_folders_loading_folders),
  556. true
  557. );
  558. }
  559. private void showEmptyContent(String headline, String message, boolean loading) {
  560. if (emptyContentContainer != null) {
  561. emptyContentContainer.setVisibility(View.VISIBLE);
  562. mRecyclerView.setVisibility(View.GONE);
  563. emptyContentHeadline.setText(headline);
  564. emptyContentMessage.setText(message);
  565. emptyContentMessage.setVisibility(View.VISIBLE);
  566. if (loading) {
  567. emptyContentProgressBar.setVisibility(View.VISIBLE);
  568. emptyContentIcon.setVisibility(View.GONE);
  569. } else {
  570. emptyContentProgressBar.setVisibility(View.GONE);
  571. emptyContentIcon.setVisibility(View.VISIBLE);
  572. }
  573. }
  574. }
  575. @Override
  576. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  577. if (requestCode == SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_REMOTE_FOLDER
  578. && resultCode == RESULT_OK && mSyncedFolderPreferencesDialogFragment != null) {
  579. OCFile chosenFolder = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER);
  580. mSyncedFolderPreferencesDialogFragment.setRemoteFolderSummary(chosenFolder.getRemotePath());
  581. }
  582. if (requestCode == SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_LOCAL_FOLDER
  583. && resultCode == RESULT_OK && mSyncedFolderPreferencesDialogFragment != null) {
  584. String localPath = data.getStringExtra(UploadFilesActivity.EXTRA_CHOSEN_FILES);
  585. mSyncedFolderPreferencesDialogFragment.setLocalFolderSummary(localPath);
  586. } else {
  587. super.onActivityResult(requestCode, resultCode, data);
  588. }
  589. }
  590. @Override
  591. public void onSaveSyncedFolderPreference(SyncedFolderParcelable syncedFolder) {
  592. // custom folders newly created aren't in the list already,
  593. // so triggering a refresh
  594. if (MediaFolderType.CUSTOM == syncedFolder.getType() && syncedFolder.getId() == UNPERSISTED_ID) {
  595. SyncedFolderDisplayItem newCustomFolder = new SyncedFolderDisplayItem(
  596. SyncedFolder.UNPERSISTED_ID, syncedFolder.getLocalPath(), syncedFolder.getRemotePath(),
  597. syncedFolder.isWifiOnly(), syncedFolder.isChargingOnly(), syncedFolder.isSubfolderByDate(),
  598. syncedFolder.getAccount(), syncedFolder.getUploadAction(), syncedFolder.isEnabled(),
  599. clock.getCurrentTime(), new File(syncedFolder.getLocalPath()).getName(), syncedFolder.getType(), syncedFolder.isHidden());
  600. saveOrUpdateSyncedFolder(newCustomFolder);
  601. mAdapter.addSyncFolderItem(newCustomFolder);
  602. } else {
  603. SyncedFolderDisplayItem item = mAdapter.get(syncedFolder.getSection());
  604. updateSyncedFolderItem(item, syncedFolder.getId(), syncedFolder.getLocalPath(),
  605. syncedFolder.getRemotePath(), syncedFolder
  606. .isWifiOnly(), syncedFolder.isChargingOnly(), syncedFolder.isSubfolderByDate(), syncedFolder
  607. .getUploadAction(), syncedFolder.isEnabled());
  608. saveOrUpdateSyncedFolder(item);
  609. // TODO test if notifiyItemChanged is suffiecient (should improve performance)
  610. mAdapter.notifyDataSetChanged();
  611. }
  612. mSyncedFolderPreferencesDialogFragment = null;
  613. if (syncedFolder.isEnabled()) {
  614. showBatteryOptimizationInfo();
  615. }
  616. }
  617. private void saveOrUpdateSyncedFolder(SyncedFolderDisplayItem item) {
  618. if (item.getId() == UNPERSISTED_ID) {
  619. // newly set up folder sync config
  620. storeSyncedFolder(item);
  621. } else {
  622. // existing synced folder setup to be updated
  623. mSyncedFolderProvider.updateSyncFolder(item);
  624. if (item.isEnabled()) {
  625. FilesSyncHelper.insertAllDBEntriesForSyncedFolder(item);
  626. } else {
  627. String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
  628. ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(MainApp.getAppContext().
  629. getContentResolver());
  630. arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);
  631. }
  632. }
  633. }
  634. private void storeSyncedFolder(SyncedFolderDisplayItem item) {
  635. ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(MainApp.getAppContext().
  636. getContentResolver());
  637. long storedId = mSyncedFolderProvider.storeSyncedFolder(item);
  638. if (storedId != -1) {
  639. item.setId(storedId);
  640. if (item.isEnabled()) {
  641. FilesSyncHelper.insertAllDBEntriesForSyncedFolder(item);
  642. } else {
  643. String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
  644. arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);
  645. }
  646. }
  647. }
  648. @Override
  649. public void onCancelSyncedFolderPreference() {
  650. mSyncedFolderPreferencesDialogFragment = null;
  651. }
  652. @Override
  653. public void onDeleteSyncedFolderPreference(SyncedFolderParcelable syncedFolder) {
  654. mSyncedFolderProvider.deleteSyncedFolder(syncedFolder.getId());
  655. mAdapter.removeItem(syncedFolder.getSection());
  656. }
  657. /**
  658. * update given synced folder with the given values.
  659. *
  660. * @param item the synced folder to be updated
  661. * @param localPath the local path
  662. * @param remotePath the remote path
  663. * @param wifiOnly upload on wifi only
  664. * @param chargingOnly upload on charging only
  665. * @param subfolderByDate created sub folders
  666. * @param uploadAction upload action
  667. * @param enabled is sync enabled
  668. */
  669. private void updateSyncedFolderItem(SyncedFolderDisplayItem item,
  670. long id,
  671. String localPath,
  672. String remotePath,
  673. boolean wifiOnly,
  674. boolean chargingOnly,
  675. boolean subfolderByDate,
  676. Integer uploadAction,
  677. boolean enabled) {
  678. item.setId(id);
  679. item.setLocalPath(localPath);
  680. item.setRemotePath(remotePath);
  681. item.setWifiOnly(wifiOnly);
  682. item.setChargingOnly(chargingOnly);
  683. item.setSubfolderByDate(subfolderByDate);
  684. item.setUploadAction(uploadAction);
  685. item.setEnabled(enabled, clock.getCurrentTime());
  686. }
  687. @Override
  688. public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
  689. @NonNull int[] grantResults) {
  690. switch (requestCode) {
  691. case PermissionUtil.PERMISSIONS_WRITE_EXTERNAL_STORAGE: {
  692. // If request is cancelled, result arrays are empty.
  693. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  694. // permission was granted
  695. int gridWidth = getResources().getInteger(R.integer.media_grid_width);
  696. load(gridWidth * 2, true);
  697. } else {
  698. // permission denied --> do nothing
  699. return;
  700. }
  701. return;
  702. }
  703. default:
  704. super.onRequestPermissionsResult(requestCode, permissions, grantResults);
  705. }
  706. }
  707. @Override
  708. protected void onResume() {
  709. super.onResume();
  710. setDrawerMenuItemChecked(R.id.nav_synced_folders);
  711. }
  712. private void showBatteryOptimizationInfo() {
  713. if (powerManagementService.isPowerSavingExclusionAvailable() || checkIfBatteryOptimizationEnabled()) {
  714. AlertDialog alertDialog = new AlertDialog.Builder(this, R.style.Theme_ownCloud_Dialog)
  715. .setTitle(getString(R.string.battery_optimization_title))
  716. .setMessage(getString(R.string.battery_optimization_message))
  717. .setPositiveButton(getString(R.string.battery_optimization_disable), (dialog, which) -> {
  718. // show instant upload
  719. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  720. @SuppressLint("BatteryLife")
  721. Intent intent = new Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
  722. Uri.parse("package:" + BuildConfig.APPLICATION_ID));
  723. if (intent.resolveActivity(getPackageManager()) != null) {
  724. startActivity(intent);
  725. }
  726. } else {
  727. Intent powerUsageIntent = new Intent(Intent.ACTION_POWER_USAGE_SUMMARY);
  728. if (getPackageManager().resolveActivity(powerUsageIntent, 0) != null) {
  729. startActivity(powerUsageIntent);
  730. } else {
  731. dialog.dismiss();
  732. DisplayUtils.showSnackMessage(this, getString(R.string.battery_optimization_no_setting));
  733. }
  734. }
  735. })
  736. .setNegativeButton(getString(R.string.battery_optimization_close), (dialog, which) -> dialog.dismiss())
  737. .setIcon(R.drawable.ic_battery_alert)
  738. .show();
  739. int color = ThemeUtils.primaryAccentColor(this);
  740. alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(color);
  741. alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(color);
  742. }
  743. }
  744. /**
  745. * Check if battery optimization is enabled. If unknown, fallback to true.
  746. *
  747. * @return true if battery optimization is enabled
  748. */
  749. private boolean checkIfBatteryOptimizationEnabled() {
  750. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  751. PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
  752. if (powerManager == null) {
  753. return true;
  754. }
  755. return !powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID);
  756. } else {
  757. return true;
  758. }
  759. }
  760. }